diff --git a/lib/ansible/modules/storage/netapp/na_ontap_gather_facts.py b/lib/ansible/modules/storage/netapp/na_ontap_gather_facts.py index fe11bd28b7..36ca0a794d 100644 --- a/lib/ansible/modules/storage/netapp/na_ontap_gather_facts.py +++ b/lib/ansible/modules/storage/netapp/na_ontap_gather_facts.py @@ -32,12 +32,12 @@ options: description: - When supplied, this argument will restrict the facts collected 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", "nvme_namespace_info", "nvme_subsystem_info", "ontap_version", - "security_key_manager_key_info", "security_login_account_info", - "storage_failover_info", "volume_info", "vserver_info", - "vserver_login_banner_info", "vserver_motd_info" + "qos_adaptive_policy_info", "qos_policy_info", "security_key_manager_key_info", + "security_login_account_info", "storage_failover_info", "volume_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 with an initial C(M(!)) to specify that a specific subset should not be collected. @@ -103,7 +103,10 @@ ontap_facts: "vserver_login_banner_info": {...}, "vserver_motd_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 with gather facts methods''' def __init__(self, module): self.module = module @@ -278,6 +282,37 @@ class NetAppONTAPGatherFacts(object): }, '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 'nvme_info': { 'method': self.get_generic_get_iter, @@ -327,34 +362,41 @@ class NetAppONTAPGatherFacts(object): self.server = netapp_utils.setup_na_ontap_zapi(module=self.module) def ontapi(self): + '''Method to get ontapi version''' + api = 'system-get-ontapi-version' api_call = netapp_utils.zapi.NaElement(api) try: results = self.server.invoke_successfully(api_call, enable_tunneling=False) ontapi_version = results.get_child_content('minor-version') 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" % - (api, to_native(e)), exception=traceback.format_exc()) + (api, to_native(error)), exception=traceback.format_exc()) def call_api(self, call, query=None): + '''Main method to run an API call''' + api_call = netapp_utils.zapi.NaElement(call) result = None if query: - for k, v in query.items(): - # Can v be nested? - api_call.add_new_child(k, v) + for key, val in query.items(): + # Can val be nested? + api_call.add_new_child(key, val) try: result = self.server.invoke_successfully(api_call, enable_tunneling=False) 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']: return result 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): + '''Method to get network port ifgroups info''' + try: net_port_info = self.netapp_info['net_port_info'] except KeyError: @@ -372,14 +414,22 @@ class NetAppONTAPGatherFacts(object): query = dict() 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.update(tmp) 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) + if call == 'net-port-ifgrp-get': + children = 'attributes' + else: + children = 'attributes-list' + if generic_call is None: return None @@ -394,25 +444,27 @@ class NetAppONTAPGatherFacts(object): return None 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: - d = d[attribute] + dic = dic[attribute] if isinstance(field, str): - unique_key = _finditem(d, field) + unique_key = _finditem(dic, field) 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): - unique_key = ':'.join([_finditem(d, el) for el in field]) + unique_key = ':'.join([_finditem(dic, el) for el in field]) 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: - out.append(convert_keys(json.loads(json.dumps(d)))) + out.append(convert_keys(json.loads(json.dumps(dic)))) return out def get_all(self, gather_subset): + '''Method to get all subsets''' + results = netapp_utils.get_cserver(self.server) cserver = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=results) netapp_utils.ems_log_event("na_ontap_gather_facts", cserver) @@ -430,6 +482,8 @@ class NetAppONTAPGatherFacts(object): return self.netapp_info def get_subset(self, gather_subset, version): + '''Method to get a single subset''' + runable_subsets = set() exclude_subsets = set() 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: return obj[key] - for dummy, v in obj.items(): - if isinstance(v, dict): - item = __finditem(v, key) + for dummy, val in obj.items(): + if isinstance(val, dict): + item = __finditem(val, key) if item is not None: return item return None @@ -487,18 +541,22 @@ def _finditem(obj, key): raise KeyError(key) -def convert_keys(d): +def convert_keys(d_param): + '''Method to convert hyphen to underscore''' + out = {} - if isinstance(d, dict): - for k, v in d.items(): - v = convert_keys(v) - out[k.replace('-', '_')] = v + if isinstance(d_param, dict): + for key, val in d_param.items(): + val = convert_keys(val) + out[key.replace('-', '_')] = val else: - return d + return d_param return out def main(): + '''Execute action''' + argument_spec = netapp_utils.na_ontap_host_argument_spec() argument_spec.update(dict( state=dict(default='info', choices=['info']), @@ -520,10 +578,10 @@ def main(): gather_subset = module.params['gather_subset'] if gather_subset is None: gather_subset = ['all'] - v = NetAppONTAPGatherFacts(module) - g = v.get_all(gather_subset) + gf_obj = NetAppONTAPGatherFacts(module) + gf_all = gf_obj.get_all(gather_subset) 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__': diff --git a/lib/ansible/modules/storage/netapp/na_ontap_qtree.py b/lib/ansible/modules/storage/netapp/na_ontap_qtree.py index 8ab0c8b9e2..2be1ed996b 100644 --- a/lib/ansible/modules/storage/netapp/na_ontap_qtree.py +++ b/lib/ansible/modules/storage/netapp/na_ontap_qtree.py @@ -2,11 +2,9 @@ # (c) 2018-2019, NetApp, Inc # 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 __metaclass__ = type - ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'certified'} @@ -46,12 +44,35 @@ options: flexvol_name: description: - The name of the FlexVol the qtree should exist on. Required when C(state=present). + required: true vserver: description: - The name of the vserver to use. 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 = """ @@ -81,26 +102,31 @@ RETURN = """ """ import traceback - from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_native import ansible.module_utils.netapp as netapp_utils - +from ansible.module_utils.netapp_module import NetAppModule HAS_NETAPP_LIB = netapp_utils.has_netapp_lib() class NetAppOntapQTree(object): + '''Class with qtree operations''' def __init__(self): self.argument_spec = netapp_utils.na_ontap_host_argument_spec() self.argument_spec.update(dict( - state=dict(required=False, choices=[ - 'present', 'absent'], default='present'), + state=dict(required=False, + choices=['present', 'absent'], + default='present'), name=dict(required=True, type='str'), from_name=dict(required=False, type='str'), flexvol_name=dict(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( @@ -110,81 +136,93 @@ class NetAppOntapQTree(object): ], supports_check_mode=True ) - - p = 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'] + self.na_helper = NetAppModule() + self.parameters = self.na_helper.set_parameters(self.module.params) if HAS_NETAPP_LIB is False: self.module.fail_json( msg="the python NetApp-Lib module is required") else: 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): """ Checks if the qtree exists. - + :param: + name : qtree name :return: - True if qtree found + Details about the qtree False if qtree is not found :rtype: bool """ if name is None: - name = self.name + name = self.parameters['name'] qtree_list_iter = netapp_utils.zapi.NaElement('qtree-list-iter') query_details = netapp_utils.zapi.NaElement.create_node_with_children( - 'qtree-info', **{'vserver': self.vserver, - 'volume': self.flexvol_name, + 'qtree-info', **{'vserver': self.parameters['vserver'], + 'volume': self.parameters['flexvol_name'], 'qtree': name}) - query = netapp_utils.zapi.NaElement('query') query.add_child_elem(query_details) qtree_list_iter.add_child_elem(query) - result = self.server.invoke_successfully(qtree_list_iter, enable_tunneling=True) - + return_q = False if (result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) >= 1): - return True - else: - return False + return_q = {'export_policy': result['attributes-list']['qtree-info']['export-policy'], + 'unix_permissions': result['attributes-list']['qtree-info']['mode'], + 'oplocks': result['attributes-list']['qtree-info']['oplocks'], + 'security_style': result['attributes-list']['qtree-info']['security-style']} + + return return_q 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', **{'volume': self.flexvol_name, - 'qtree': self.name}) - + 'qtree-create', **options) try: self.server.invoke_successfully(qtree_create, enable_tunneling=True) - except netapp_utils.zapi.NaApiError as e: - self.module.fail_json(msg="Error provisioning qtree %s: %s" % (self.name, to_native(e)), + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg="Error provisioning qtree %s: %s" + % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) 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', **{'qtree': path}) try: self.server.invoke_successfully(qtree_delete, enable_tunneling=True) - except netapp_utils.zapi.NaApiError as e: - self.module.fail_json(msg="Error deleting qtree %s: %s" % (path, to_native(e)), + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg="Error deleting qtree %s: %s" % (path, to_native(error)), exception=traceback.format_exc()) 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', **{'qtree': path, 'new-qtree-name': new_path}) @@ -192,25 +230,57 @@ class NetAppOntapQTree(object): try: self.server.invoke_successfully(qtree_rename, enable_tunneling=True) - except netapp_utils.zapi.NaApiError as e: - self.module.fail_json(msg="Error renaming qtree %s: %s" % (self.from_name, to_native(e)), + except netapp_utils.zapi.NaApiError as error: + 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()) def apply(self): - changed = False - qtree_exists = False - rename_qtree = False + '''Call create/delete/modify/rename operations''' + # changed = 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) qtree_detail = self.get_qtree() if qtree_detail: - qtree_exists = True - if self.state == 'absent': # delete + # delete or modify qtree + if self.parameters['state'] == 'absent': # delete 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 - if self.from_name: - if self.get_qtree(self.from_name) is None: - self.module.fail_json(msg="Error renaming qtree %s: does not exists" % self.from_name) + if self.parameters.get('from_name'): + if self.get_qtree(self.parameters['from_name']) is None: + self.module.fail_json( + msg="Error renaming qtree %s: does not exists" + % self.parameters['from_name']) else: changed = True rename_qtree = True @@ -220,20 +290,23 @@ class NetAppOntapQTree(object): if self.module.check_mode: pass else: - if self.state == 'present': + if self.parameters['state'] == 'present': if rename_qtree: self.rename_qtree() + elif modified_qtree: + self.modify_qtree() else: self.create_qtree() - elif self.state == 'absent': + elif self.parameters['state'] == 'absent': self.delete_qtree() self.module.exit_json(changed=changed) def main(): - v = NetAppOntapQTree() - v.apply() + '''Apply qtree operations from playbook''' + qtree_obj = NetAppOntapQTree() + qtree_obj.apply() if __name__ == '__main__':