na_ontap_qtree / na_ontap_gather_facts: qtree new params and modify operation / new subsets (#55825)

* qtree new parameters and modify action

* fixing pylint offenses

* fixing shippable fails

* added igroup_info gather_subset

* added qos_policy_info / qos_adaptive_policy_info gather_subsets

* pylint fixes

* fixing shippable test failure

* requiring option flexvol_name for na_ontap_qtree module
This commit is contained in:
vicmunoz 2019-05-07 20:32:27 +02:00 committed by Jake Jackson
parent ba9fee6c37
commit 6f0ac90ec3
2 changed files with 216 additions and 85 deletions

View file

@ -32,12 +32,12 @@ options:
description: description:
- When supplied, this argument will restrict the facts collected - When supplied, this argument will restrict the facts collected
to a given subset. Possible values for this argument include to a given subset. Possible values for this argument include
"aggregate_info", "cluster_node_info", "lun_info", "net_ifgrp_info", "aggregate_info", "cluster_node_info", "igroup_info", "lun_info", "net_ifgrp_info",
"net_interface_info", "net_port_info", "nvme_info", "nvme_interface_info", "net_interface_info", "net_port_info", "nvme_info", "nvme_interface_info",
"nvme_namespace_info", "nvme_subsystem_info", "ontap_version", "nvme_namespace_info", "nvme_subsystem_info", "ontap_version",
"security_key_manager_key_info", "security_login_account_info", "qos_adaptive_policy_info", "qos_policy_info", "security_key_manager_key_info",
"storage_failover_info", "volume_info", "vserver_info", "security_login_account_info", "storage_failover_info", "volume_info",
"vserver_login_banner_info", "vserver_motd_info" "vserver_info", "vserver_login_banner_info", "vserver_motd_info"
Can specify a list of values to include a larger subset. Values can also be used Can specify a list of values to include a larger subset. Values can also be used
with an initial C(M(!)) to specify that a specific subset should with an initial C(M(!)) to specify that a specific subset should
not be collected. not be collected.
@ -103,7 +103,10 @@ ontap_facts:
"vserver_login_banner_info": {...}, "vserver_login_banner_info": {...},
"vserver_motd_info": {...}, "vserver_motd_info": {...},
"vserver_info": {...}, "vserver_info": {...},
"ontap_version": {...} "ontap_version": {...},
"igroup_info": {...},
"qos_policy_info": {...},
"qos_adaptive_policy_info": {...}
}' }'
''' '''
@ -128,6 +131,7 @@ HAS_NETAPP_LIB = netapp_utils.has_netapp_lib()
class NetAppONTAPGatherFacts(object): class NetAppONTAPGatherFacts(object):
'''Class with gather facts methods'''
def __init__(self, module): def __init__(self, module):
self.module = module self.module = module
@ -278,6 +282,37 @@ class NetAppONTAPGatherFacts(object):
}, },
'min_version': '0', 'min_version': '0',
}, },
'igroup_info': {
'method': self.get_generic_get_iter,
'kwargs': {
'call': 'igroup-get-iter',
'attribute': 'initiator-group-info',
'field': ('vserver', 'initiator-group-name'),
'query': {'max-records': '1024'},
},
'min_version': '0',
},
'qos_policy_info': {
'method': self.get_generic_get_iter,
'kwargs': {
'call': 'qos-policy-group-get-iter',
'attribute': 'qos-policy-group-info',
'field': 'policy-group',
'query': {'max-records': '1024'},
},
'min_version': '0',
},
# supported in ONTAP 9.3 and onwards
'qos_adaptive_policy_info': {
'method': self.get_generic_get_iter,
'kwargs': {
'call': 'qos-adaptive-policy-group-get-iter',
'attribute': 'qos-adaptive-policy-group-info',
'field': 'policy-group',
'query': {'max-records': '1024'},
},
'min_version': '130',
},
# supported in ONTAP 9.4 and onwards # supported in ONTAP 9.4 and onwards
'nvme_info': { 'nvme_info': {
'method': self.get_generic_get_iter, 'method': self.get_generic_get_iter,
@ -327,34 +362,41 @@ class NetAppONTAPGatherFacts(object):
self.server = netapp_utils.setup_na_ontap_zapi(module=self.module) self.server = netapp_utils.setup_na_ontap_zapi(module=self.module)
def ontapi(self): def ontapi(self):
'''Method to get ontapi version'''
api = 'system-get-ontapi-version' api = 'system-get-ontapi-version'
api_call = netapp_utils.zapi.NaElement(api) api_call = netapp_utils.zapi.NaElement(api)
try: try:
results = self.server.invoke_successfully(api_call, enable_tunneling=False) results = self.server.invoke_successfully(api_call, enable_tunneling=False)
ontapi_version = results.get_child_content('minor-version') ontapi_version = results.get_child_content('minor-version')
return ontapi_version if ontapi_version is not None else '0' return ontapi_version if ontapi_version is not None else '0'
except netapp_utils.zapi.NaApiError as e: except netapp_utils.zapi.NaApiError as error:
self.module.fail_json(msg="Error calling API %s: %s" % self.module.fail_json(msg="Error calling API %s: %s" %
(api, to_native(e)), exception=traceback.format_exc()) (api, to_native(error)), exception=traceback.format_exc())
def call_api(self, call, query=None): def call_api(self, call, query=None):
'''Main method to run an API call'''
api_call = netapp_utils.zapi.NaElement(call) api_call = netapp_utils.zapi.NaElement(call)
result = None result = None
if query: if query:
for k, v in query.items(): for key, val in query.items():
# Can v be nested? # Can val be nested?
api_call.add_new_child(k, v) api_call.add_new_child(key, val)
try: try:
result = self.server.invoke_successfully(api_call, enable_tunneling=False) result = self.server.invoke_successfully(api_call, enable_tunneling=False)
return result return result
except netapp_utils.zapi.NaApiError as e: except netapp_utils.zapi.NaApiError as error:
if call in ['security-key-manager-key-get-iter']: if call in ['security-key-manager-key-get-iter']:
return result return result
else: else:
self.module.fail_json(msg="Error calling API %s: %s" % (call, to_native(e)), exception=traceback.format_exc()) self.module.fail_json(msg="Error calling API %s: %s"
% (call, to_native(error)), exception=traceback.format_exc())
def get_ifgrp_info(self): def get_ifgrp_info(self):
'''Method to get network port ifgroups info'''
try: try:
net_port_info = self.netapp_info['net_port_info'] net_port_info = self.netapp_info['net_port_info']
except KeyError: except KeyError:
@ -372,14 +414,22 @@ class NetAppONTAPGatherFacts(object):
query = dict() query = dict()
query['node'], query['ifgrp-name'] = ifgrp.split(':') query['node'], query['ifgrp-name'] = ifgrp.split(':')
tmp = self.get_generic_get_iter('net-port-ifgrp-get', field=('node', 'ifgrp-name'), attribute='net-ifgrp-info', query=query, children='attributes') tmp = self.get_generic_get_iter('net-port-ifgrp-get', field=('node', 'ifgrp-name'),
attribute='net-ifgrp-info', query=query)
net_ifgrp_info = net_ifgrp_info.copy() net_ifgrp_info = net_ifgrp_info.copy()
net_ifgrp_info.update(tmp) net_ifgrp_info.update(tmp)
return net_ifgrp_info return net_ifgrp_info
def get_generic_get_iter(self, call, attribute=None, field=None, query=None, children='attributes-list'): def get_generic_get_iter(self, call, attribute=None, field=None, query=None):
'''Method to run a generic get-iter call'''
generic_call = self.call_api(call, query) generic_call = self.call_api(call, query)
if call == 'net-port-ifgrp-get':
children = 'attributes'
else:
children = 'attributes-list'
if generic_call is None: if generic_call is None:
return None return None
@ -394,25 +444,27 @@ class NetAppONTAPGatherFacts(object):
return None return None
for child in attributes_list.get_children(): for child in attributes_list.get_children():
d = xmltodict.parse(child.to_string(), xml_attribs=False) dic = xmltodict.parse(child.to_string(), xml_attribs=False)
if attribute is not None: if attribute is not None:
d = d[attribute] dic = dic[attribute]
if isinstance(field, str): if isinstance(field, str):
unique_key = _finditem(d, field) unique_key = _finditem(dic, field)
out = out.copy() out = out.copy()
out.update({unique_key: convert_keys(json.loads(json.dumps(d)))}) out.update({unique_key: convert_keys(json.loads(json.dumps(dic)))})
elif isinstance(field, tuple): elif isinstance(field, tuple):
unique_key = ':'.join([_finditem(d, el) for el in field]) unique_key = ':'.join([_finditem(dic, el) for el in field])
out = out.copy() out = out.copy()
out.update({unique_key: convert_keys(json.loads(json.dumps(d)))}) out.update({unique_key: convert_keys(json.loads(json.dumps(dic)))})
else: else:
out.append(convert_keys(json.loads(json.dumps(d)))) out.append(convert_keys(json.loads(json.dumps(dic))))
return out return out
def get_all(self, gather_subset): def get_all(self, gather_subset):
'''Method to get all subsets'''
results = netapp_utils.get_cserver(self.server) results = netapp_utils.get_cserver(self.server)
cserver = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=results) cserver = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=results)
netapp_utils.ems_log_event("na_ontap_gather_facts", cserver) netapp_utils.ems_log_event("na_ontap_gather_facts", cserver)
@ -430,6 +482,8 @@ class NetAppONTAPGatherFacts(object):
return self.netapp_info return self.netapp_info
def get_subset(self, gather_subset, version): def get_subset(self, gather_subset, version):
'''Method to get a single subset'''
runable_subsets = set() runable_subsets = set()
exclude_subsets = set() exclude_subsets = set()
usable_subsets = [key for key in self.fact_subsets.keys() if version >= self.fact_subsets[key]['min_version']] usable_subsets = [key for key in self.fact_subsets.keys() if version >= self.fact_subsets[key]['min_version']]
@ -471,9 +525,9 @@ def __finditem(obj, key):
if key in obj: if key in obj:
return obj[key] return obj[key]
for dummy, v in obj.items(): for dummy, val in obj.items():
if isinstance(v, dict): if isinstance(val, dict):
item = __finditem(v, key) item = __finditem(val, key)
if item is not None: if item is not None:
return item return item
return None return None
@ -487,18 +541,22 @@ def _finditem(obj, key):
raise KeyError(key) raise KeyError(key)
def convert_keys(d): def convert_keys(d_param):
'''Method to convert hyphen to underscore'''
out = {} out = {}
if isinstance(d, dict): if isinstance(d_param, dict):
for k, v in d.items(): for key, val in d_param.items():
v = convert_keys(v) val = convert_keys(val)
out[k.replace('-', '_')] = v out[key.replace('-', '_')] = val
else: else:
return d return d_param
return out return out
def main(): def main():
'''Execute action'''
argument_spec = netapp_utils.na_ontap_host_argument_spec() argument_spec = netapp_utils.na_ontap_host_argument_spec()
argument_spec.update(dict( argument_spec.update(dict(
state=dict(default='info', choices=['info']), state=dict(default='info', choices=['info']),
@ -520,10 +578,10 @@ def main():
gather_subset = module.params['gather_subset'] gather_subset = module.params['gather_subset']
if gather_subset is None: if gather_subset is None:
gather_subset = ['all'] gather_subset = ['all']
v = NetAppONTAPGatherFacts(module) gf_obj = NetAppONTAPGatherFacts(module)
g = v.get_all(gather_subset) gf_all = gf_obj.get_all(gather_subset)
result = {'state': state, 'changed': False} result = {'state': state, 'changed': False}
module.exit_json(ansible_facts={'ontap_facts': g}, **result) module.exit_json(ansible_facts={'ontap_facts': gf_all}, **result)
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -2,11 +2,9 @@
# (c) 2018-2019, NetApp, Inc # (c) 2018-2019, NetApp, Inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1', ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'], 'status': ['preview'],
'supported_by': 'certified'} 'supported_by': 'certified'}
@ -46,12 +44,35 @@ options:
flexvol_name: flexvol_name:
description: description:
- The name of the FlexVol the qtree should exist on. Required when C(state=present). - The name of the FlexVol the qtree should exist on. Required when C(state=present).
required: true
vserver: vserver:
description: description:
- The name of the vserver to use. - The name of the vserver to use.
required: true required: true
export_policy:
description:
- The name of the export policy to apply.
version_added: '2.9'
security_style:
description:
- The security style for the qtree.
choices: ['unix', 'ntfs', 'mixed']
version_added: '2.9'
oplocks:
description:
- Whether the oplocks should be enabled or not for the qtree.
choices: ['enabled', 'disabled']
version_added: '2.9'
unix_permissions:
description:
- File permissions bits of the qtree.
version_added: '2.9'
''' '''
EXAMPLES = """ EXAMPLES = """
@ -81,26 +102,31 @@ RETURN = """
""" """
import traceback import traceback
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
import ansible.module_utils.netapp as netapp_utils import ansible.module_utils.netapp as netapp_utils
from ansible.module_utils.netapp_module import NetAppModule
HAS_NETAPP_LIB = netapp_utils.has_netapp_lib() HAS_NETAPP_LIB = netapp_utils.has_netapp_lib()
class NetAppOntapQTree(object): class NetAppOntapQTree(object):
'''Class with qtree operations'''
def __init__(self): def __init__(self):
self.argument_spec = netapp_utils.na_ontap_host_argument_spec() self.argument_spec = netapp_utils.na_ontap_host_argument_spec()
self.argument_spec.update(dict( self.argument_spec.update(dict(
state=dict(required=False, choices=[ state=dict(required=False,
'present', 'absent'], default='present'), choices=['present', 'absent'],
default='present'),
name=dict(required=True, type='str'), name=dict(required=True, type='str'),
from_name=dict(required=False, type='str'), from_name=dict(required=False, type='str'),
flexvol_name=dict(type='str'), flexvol_name=dict(type='str'),
vserver=dict(required=True, type='str'), vserver=dict(required=True, type='str'),
export_policy=dict(required=False, type='str'),
security_style=dict(required=False, choices=['unix', 'ntfs', 'mixed']),
oplocks=dict(required=False, choices=['enabled', 'disabled']),
unix_permissions=dict(required=False, type='str'),
)) ))
self.module = AnsibleModule( self.module = AnsibleModule(
@ -110,81 +136,93 @@ class NetAppOntapQTree(object):
], ],
supports_check_mode=True supports_check_mode=True
) )
self.na_helper = NetAppModule()
p = self.module.params self.parameters = self.na_helper.set_parameters(self.module.params)
# set up state variables
self.state = p['state']
self.name = p['name']
self.from_name = p['from_name']
self.flexvol_name = p['flexvol_name']
self.vserver = p['vserver']
if HAS_NETAPP_LIB is False: if HAS_NETAPP_LIB is False:
self.module.fail_json( self.module.fail_json(
msg="the python NetApp-Lib module is required") msg="the python NetApp-Lib module is required")
else: else:
self.server = netapp_utils.setup_na_ontap_zapi( self.server = netapp_utils.setup_na_ontap_zapi(
module=self.module, vserver=self.vserver) module=self.module, vserver=self.parameters['vserver'])
def get_qtree(self, name=None): def get_qtree(self, name=None):
""" """
Checks if the qtree exists. Checks if the qtree exists.
:param:
name : qtree name
:return: :return:
True if qtree found Details about the qtree
False if qtree is not found False if qtree is not found
:rtype: bool :rtype: bool
""" """
if name is None: if name is None:
name = self.name name = self.parameters['name']
qtree_list_iter = netapp_utils.zapi.NaElement('qtree-list-iter') qtree_list_iter = netapp_utils.zapi.NaElement('qtree-list-iter')
query_details = netapp_utils.zapi.NaElement.create_node_with_children( query_details = netapp_utils.zapi.NaElement.create_node_with_children(
'qtree-info', **{'vserver': self.vserver, 'qtree-info', **{'vserver': self.parameters['vserver'],
'volume': self.flexvol_name, 'volume': self.parameters['flexvol_name'],
'qtree': name}) 'qtree': name})
query = netapp_utils.zapi.NaElement('query') query = netapp_utils.zapi.NaElement('query')
query.add_child_elem(query_details) query.add_child_elem(query_details)
qtree_list_iter.add_child_elem(query) qtree_list_iter.add_child_elem(query)
result = self.server.invoke_successfully(qtree_list_iter, result = self.server.invoke_successfully(qtree_list_iter,
enable_tunneling=True) enable_tunneling=True)
return_q = False
if (result.get_child_by_name('num-records') and if (result.get_child_by_name('num-records') and
int(result.get_child_content('num-records')) >= 1): int(result.get_child_content('num-records')) >= 1):
return True return_q = {'export_policy': result['attributes-list']['qtree-info']['export-policy'],
else: 'unix_permissions': result['attributes-list']['qtree-info']['mode'],
return False 'oplocks': result['attributes-list']['qtree-info']['oplocks'],
'security_style': result['attributes-list']['qtree-info']['security-style']}
return return_q
def create_qtree(self): def create_qtree(self):
"""
Create a qtree
"""
options = {'qtree': self.parameters['name'], 'volume': self.parameters['flexvol_name']}
if self.parameters.get('export_policy'):
options['export-policy'] = self.parameters['export_policy']
if self.parameters.get('security_style'):
options['security-style'] = self.parameters['security_style']
if self.parameters.get('oplocks'):
options['oplocks'] = self.parameters['oplocks']
if self.parameters.get('unix_permissions'):
options['mode'] = self.parameters['unix_permissions']
qtree_create = netapp_utils.zapi.NaElement.create_node_with_children( qtree_create = netapp_utils.zapi.NaElement.create_node_with_children(
'qtree-create', **{'volume': self.flexvol_name, 'qtree-create', **options)
'qtree': self.name})
try: try:
self.server.invoke_successfully(qtree_create, self.server.invoke_successfully(qtree_create,
enable_tunneling=True) enable_tunneling=True)
except netapp_utils.zapi.NaApiError as e: except netapp_utils.zapi.NaApiError as error:
self.module.fail_json(msg="Error provisioning qtree %s: %s" % (self.name, to_native(e)), self.module.fail_json(msg="Error provisioning qtree %s: %s"
% (self.parameters['name'], to_native(error)),
exception=traceback.format_exc()) exception=traceback.format_exc())
def delete_qtree(self): def delete_qtree(self):
path = '/vol/%s/%s' % (self.flexvol_name, self.name) """
Delete a qtree
"""
path = '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['name'])
qtree_delete = netapp_utils.zapi.NaElement.create_node_with_children( qtree_delete = netapp_utils.zapi.NaElement.create_node_with_children(
'qtree-delete', **{'qtree': path}) 'qtree-delete', **{'qtree': path})
try: try:
self.server.invoke_successfully(qtree_delete, self.server.invoke_successfully(qtree_delete,
enable_tunneling=True) enable_tunneling=True)
except netapp_utils.zapi.NaApiError as e: except netapp_utils.zapi.NaApiError as error:
self.module.fail_json(msg="Error deleting qtree %s: %s" % (path, to_native(e)), self.module.fail_json(msg="Error deleting qtree %s: %s" % (path, to_native(error)),
exception=traceback.format_exc()) exception=traceback.format_exc())
def rename_qtree(self): def rename_qtree(self):
path = '/vol/%s/%s' % (self.flexvol_name, self.from_name) """
new_path = '/vol/%s/%s' % (self.flexvol_name, self.name) Rename a qtree
"""
path = '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['from_name'])
new_path = '/vol/%s/%s' % (self.parameters['flexvol_name'], self.parameters['name'])
qtree_rename = netapp_utils.zapi.NaElement.create_node_with_children( qtree_rename = netapp_utils.zapi.NaElement.create_node_with_children(
'qtree-rename', **{'qtree': path, 'qtree-rename', **{'qtree': path,
'new-qtree-name': new_path}) 'new-qtree-name': new_path})
@ -192,25 +230,57 @@ class NetAppOntapQTree(object):
try: try:
self.server.invoke_successfully(qtree_rename, self.server.invoke_successfully(qtree_rename,
enable_tunneling=True) enable_tunneling=True)
except netapp_utils.zapi.NaApiError as e: except netapp_utils.zapi.NaApiError as error:
self.module.fail_json(msg="Error renaming qtree %s: %s" % (self.from_name, to_native(e)), self.module.fail_json(msg="Error renaming qtree %s: %s"
% (self.parameters['from_name'], to_native(error)),
exception=traceback.format_exc())
def modify_qtree(self):
"""
Modify a qtree
"""
options = {'qtree': self.parameters['name'], 'volume': self.parameters['flexvol_name']}
if self.parameters.get('export_policy'):
options['export-policy'] = self.parameters['export_policy']
if self.parameters.get('security_style'):
options['security-style'] = self.parameters['security_style']
if self.parameters.get('oplocks'):
options['oplocks'] = self.parameters['oplocks']
if self.parameters.get('unix_permissions'):
options['mode'] = self.parameters['unix_permissions']
qtree_modify = netapp_utils.zapi.NaElement.create_node_with_children(
'qtree-modify', **options)
try:
self.server.invoke_successfully(qtree_modify, enable_tunneling=True)
except netapp_utils.zapi.NaApiError as error:
self.module.fail_json(msg='Error modifying qtree %s: %s'
% (self.parameters['name'], to_native(error)),
exception=traceback.format_exc()) exception=traceback.format_exc())
def apply(self): def apply(self):
changed = False '''Call create/delete/modify/rename operations'''
qtree_exists = False # changed = False
rename_qtree = False # rename_qtree = False
# modified_qtree = None
changed, rename_qtree, modified_qtree = False, False, None
netapp_utils.ems_log_event("na_ontap_qtree", self.server) netapp_utils.ems_log_event("na_ontap_qtree", self.server)
qtree_detail = self.get_qtree() qtree_detail = self.get_qtree()
if qtree_detail: if qtree_detail:
qtree_exists = True # delete or modify qtree
if self.state == 'absent': # delete if self.parameters['state'] == 'absent': # delete
changed = True changed = True
elif self.state == 'present': else:
modified_qtree = self.na_helper.get_modified_attributes(
qtree_detail, self.parameters)
if modified_qtree is not None:
changed = True
elif self.parameters['state'] == 'present':
# create or rename qtree # create or rename qtree
if self.from_name: if self.parameters.get('from_name'):
if self.get_qtree(self.from_name) is None: if self.get_qtree(self.parameters['from_name']) is None:
self.module.fail_json(msg="Error renaming qtree %s: does not exists" % self.from_name) self.module.fail_json(
msg="Error renaming qtree %s: does not exists"
% self.parameters['from_name'])
else: else:
changed = True changed = True
rename_qtree = True rename_qtree = True
@ -220,20 +290,23 @@ class NetAppOntapQTree(object):
if self.module.check_mode: if self.module.check_mode:
pass pass
else: else:
if self.state == 'present': if self.parameters['state'] == 'present':
if rename_qtree: if rename_qtree:
self.rename_qtree() self.rename_qtree()
elif modified_qtree:
self.modify_qtree()
else: else:
self.create_qtree() self.create_qtree()
elif self.state == 'absent': elif self.parameters['state'] == 'absent':
self.delete_qtree() self.delete_qtree()
self.module.exit_json(changed=changed) self.module.exit_json(changed=changed)
def main(): def main():
v = NetAppOntapQTree() '''Apply qtree operations from playbook'''
v.apply() qtree_obj = NetAppOntapQTree()
qtree_obj.apply()
if __name__ == '__main__': if __name__ == '__main__':