diff --git a/lib/ansible/modules/storage/netapp/na_ontap_export_policy_rule.py b/lib/ansible/modules/storage/netapp/na_ontap_export_policy_rule.py index 8ed4de646c..09701cc331 100644 --- a/lib/ansible/modules/storage/netapp/na_ontap_export_policy_rule.py +++ b/lib/ansible/modules/storage/netapp/na_ontap_export_policy_rule.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2018, 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) from __future__ import absolute_import, division, print_function @@ -33,28 +33,30 @@ options: choices: ['present', 'absent'] default: present - policy_name: + name: description: - The name of the export rule to manage. required: True + aliases: + - policy_name client_match: description: - - List of Client Match Hostnames, IP Addresses, Netgroups, or Domains + - List of Client Match host names, IP Addresses, Netgroups, or Domains ro_rule: description: - - Read only access specifications for the rule + - List of Read only access specifications for the rule choices: ['any','none','never','krb5','krb5i','krb5p','ntlm','sys'] rw_rule: description: - - Read Write access specifications for the rule + - List of Read Write access specifications for the rule choices: ['any','none','never','krb5','krb5i','krb5p','ntlm','sys'] super_user_security: description: - - Read Write access specifications for the rule + - List of Read Write access specifications for the rule choices: ['any','none','never','krb5','krb5i','krb5p','ntlm','sys'] allow_suid: @@ -64,13 +66,15 @@ options: protocol: description: - - Client access protocol. Default value is 'any' + - List of Client access protocols. + - Default value is set to 'any' during create. choices: [any,nfs,nfs3,nfs4,cifs,flexcache] - default: any rule_index: description: - - rule index of the export policy for delete and modify + - rule index of the export policy + - Required for delete and modify + - If rule_index is not set for a modify, the module will create another rule with desired parameters vserver: description: @@ -83,39 +87,42 @@ EXAMPLES = """ - name: Create ExportPolicyRule na_ontap_export_policy_rule: state: present - policy_name: default123 + name: default123 vserver: ci_dev - client_match: 0.0.0.0/0 - ro_rule: any + client_match: 0.0.0.0/0,1.1.1.0/24 + ro_rule: krb5,krb5i rw_rule: any - protocol: any + protocol: nfs,nfs3 super_user_security: any allow_suid: true hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" - - name: Delete ExportPolicyRule - na_ontap_export_policy_rule: - state: absent - policy_name: default123 - hostname: "{{ netapp_hostname }}" - username: "{{ netapp_username }}" - password: "{{ netapp_password }}" - - name: Modify ExportPolicyRule na_ontap_export_policy_rule: state: present - policy_name: default123 + name: default123 + rule_index: 100 client_match: 0.0.0.0/0 - ro_rule: any + ro_rule: ntlm rw_rule: any - super_user_security: none protocol: any allow_suid: false hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" + + - name: Delete ExportPolicyRule + na_ontap_export_policy_rule: + state: absent + name: default123 + rule_index: 100 + vserver: ci_dev + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + """ RETURN = """ @@ -128,6 +135,7 @@ import json 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() @@ -141,19 +149,19 @@ class NetAppontapExportRule(object): self.argument_spec = netapp_utils.na_ontap_host_argument_spec() self.argument_spec.update(dict( state=dict(required=False, type='str', choices=['present', 'absent'], default='present'), - policy_name=dict(required=True, type='str'), + name=dict(required=True, type='str', aliases=['policy_name']), protocol=dict(required=False, - type='str', default='any', + type='list', default=None, choices=['any', 'nfs', 'nfs3', 'nfs4', 'cifs', 'flexcache']), - client_match=dict(required=False, type='str'), + client_match=dict(required=False, type='list'), ro_rule=dict(required=False, - type='str', default=None, + type='list', default=None, choices=['any', 'none', 'never', 'krb5', 'krb5i', 'krb5p', 'ntlm', 'sys']), rw_rule=dict(required=False, - type='str', default=None, + type='list', default=None, choices=['any', 'none', 'never', 'krb5', 'krb5i', 'krb5p', 'ntlm', 'sys']), super_user_security=dict(required=False, - type='str', default=None, + type='list', default=None, choices=['any', 'none', 'never', 'krb5', 'krb5i', 'krb5p', 'ntlm', 'sys']), allow_suid=dict(required=False, type='bool'), rule_index=dict(required=False, type='int'), @@ -162,33 +170,65 @@ class NetAppontapExportRule(object): self.module = AnsibleModule( argument_spec=self.argument_spec, - required_if=[ - ('state', 'present', ['client_match', 'ro_rule', 'rw_rule']), - ('state', 'absent', ['client_match']) - ], supports_check_mode=True ) - parameters = self.module.params - - # set up state variables - self.state = parameters['state'] - self.policy_name = parameters['policy_name'] - self.protocol = parameters['protocol'] - self.client_match = parameters['client_match'] - self.ro_rule = parameters['ro_rule'] - self.rw_rule = parameters['rw_rule'] - self.rule_index = parameters['rule_index'] - self.allow_suid = parameters['allow_suid'] - self.vserver = parameters['vserver'] - self.super_user_security = parameters['super_user_security'] + self.na_helper = NetAppModule() + self.parameters = self.na_helper.set_parameters(self.module.params) + self.set_playbook_zapi_key_map() 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 set_playbook_zapi_key_map(self): + self.na_helper.zapi_string_keys = { + 'client_match': 'client-match', + 'name': 'policy-name' + } + self.na_helper.zapi_list_keys = { + 'protocol': ('protocol', 'access-protocol'), + 'ro_rule': ('ro-rule', 'security-flavor'), + 'rw_rule': ('rw-rule', 'security-flavor'), + 'super_user_security': ('super-user-security', 'security-flavor'), + } + self.na_helper.zapi_bool_keys = { + 'allow_suid': 'is-allow-set-uid-enabled' + } + self.na_helper.zapi_int_keys = { + 'rule_index': 'rule-index' + } + + def set_query_parameters(self): + """ + Return dictionary of query parameters and + :return: + """ + query = { + 'policy-name': self.parameters['name'], + 'vserver': self.parameters['vserver'] + } + + if self.parameters.get('rule_index'): + query['rule-index'] = self.parameters['rule_index'] + else: + if self.parameters.get('ro_rule'): + query['ro-rule'] = {'security-flavor': self.parameters['ro_rule']} + if self.parameters.get('rw_rule'): + query['rw-rule'] = {'security-flavor': self.parameters['rw_rule']} + if self.parameters.get('protocol'): + query['protocol'] = {'security-flavor': self.parameters['protocol']} + if self.parameters.get('client_match'): + query['client-match'] = self.parameters['client_match'] + attributes = { + 'query': { + 'export-rule-info': query + } + } + return attributes def get_export_policy_rule(self): """ @@ -198,64 +238,34 @@ class NetAppontapExportRule(object): :return: Details about the export_policy. None if not found. :rtype: dict """ + current, result = None, None rule_iter = netapp_utils.zapi.NaElement('export-rule-get-iter') - rule_info = netapp_utils.zapi.NaElement('export-rule-info') - rule_info.add_new_child('policy-name', self.policy_name) - if self.vserver: - rule_info.add_new_child('vserver-name', self.vserver) + rule_iter.translate_struct(self.set_query_parameters()) + try: + result = self.server.invoke_successfully(rule_iter, True) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error getting export policy rule %s: %s' + % (self.parameters['name'], to_native(error)), + exception=traceback.format_exc()) - if self.client_match: - rule_info.add_new_child('client-match', self.client_match) - - query = netapp_utils.zapi.NaElement('query') - query.add_child_elem(rule_info) - - rule_iter.add_child_elem(query) - result = self.server.invoke_successfully(rule_iter, True) - return_value = None - - if result.get_child_by_name('num-records') and \ - int(result.get_child_content('num-records')) == 1: - - export_policy_rule_details = result.get_child_by_name('attributes-list').\ - get_child_by_name('export-rule-info') - export_policy_name = export_policy_rule_details.get_child_content( - 'policy-name') - export_rule_index = export_policy_rule_details.get_child_content( - 'rule-index') - export_rule_protocol = export_policy_rule_details.get_child_by_name( - 'protocol').get_child_content('access-protocol') - export_rule_ro_rule = export_policy_rule_details.get_child_by_name( - 'ro-rule').get_child_content('security-flavor') - export_rule_rw_rule = export_policy_rule_details.get_child_by_name( - 'rw-rule').get_child_content('security-flavor') - export_rule_super_user_security = export_policy_rule_details.get_child_by_name( - 'super-user-security').get_child_content('security-flavor') - export_rule_allow_suid = True if export_policy_rule_details.get_child_content( - 'is-allow-set-uid-enabled') == 'true' else False - export_rule_client_match = export_policy_rule_details.get_child_content( - 'client-match') - export_vserver = export_policy_rule_details.get_child_content( - 'vserver-name') - return_value = { - 'policy-name': export_policy_name, - 'rule-index': export_rule_index, - 'protocol': export_rule_protocol, - 'ro-rule': export_rule_ro_rule, - 'rw-rule': export_rule_rw_rule, - 'super-user-security': export_rule_super_user_security, - 'is-allow-set-uid-enabled': export_rule_allow_suid, - 'client-match': export_rule_client_match, - 'vserver': export_vserver - } - - elif result.get_child_by_name('num-records') and \ - int(result.get_child_content('num-records')) >= 1: - return_value = { - 'policy-name': self.policy_name - } - - return return_value + if result is not None and \ + result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) >= 1: + current = dict() + rule_info = result.get_child_by_name('attributes-list').get_child_by_name('export-rule-info') + for item_key, zapi_key in self.na_helper.zapi_string_keys.items(): + current[item_key] = rule_info.get_child_content(zapi_key) + for item_key, zapi_key in self.na_helper.zapi_bool_keys.items(): + current[item_key] = self.na_helper.get_value_for_bool(from_zapi=True, + value=rule_info[zapi_key]) + for item_key, zapi_key in self.na_helper.zapi_int_keys.items(): + current[item_key] = self.na_helper.get_value_for_int(from_zapi=True, + value=rule_info[zapi_key]) + for item_key, zapi_key in self.na_helper.zapi_list_keys.items(): + parent, dummy = zapi_key + current[item_key] = self.na_helper.get_value_for_list(from_zapi=True, + zapi_parent=rule_info.get_child_by_name(parent)) + current['num_records'] = int(result.get_child_content('num-records')) + return current def get_export_policy(self): """ @@ -266,85 +276,92 @@ class NetAppontapExportRule(object): :return: Details about the export-policy. None if not found. :rtype: dict """ - export_policy_iter = netapp_utils.zapi.NaElement( - 'export-policy-get-iter') - export_policy_info = netapp_utils.zapi.NaElement('export-policy-info') - export_policy_info.add_new_child('policy-name', self.policy_name) - export_policy_info.add_new_child('vserver', self.vserver) - - query = netapp_utils.zapi.NaElement('query') - query.add_child_elem(export_policy_info) - - export_policy_iter.add_child_elem(query) - - result = self.server.invoke_successfully(export_policy_iter, True) - - return_value = None - - # check if query returns the expected export-policy - if result.get_child_by_name('num-records') and \ - int(result.get_child_content('num-records')) == 1: - - result.get_child_by_name('attributes-list').\ - get_child_by_name('export-policy-info').\ - get_child_by_name('policy-name') - return_value = { - 'policy-name': self.policy_name + export_policy_iter = netapp_utils.zapi.NaElement('export-policy-get-iter') + attributes = { + 'query': { + 'export-policy-info': { + 'policy-name': self.parameters['name'], + 'vserver': self.parameters['vserver'] + } } + } - return return_value + export_policy_iter.translate_struct(attributes) + try: + result = self.server.invoke_successfully(export_policy_iter, True) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error getting export policy %s: %s' + % (self.parameters['name'], to_native(error)), + exception=traceback.format_exc()) + + if result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) == 1: + return result + + return None + + def add_parameters_for_create_or_modify(self, na_element_object, values): + """ + Add children node for create or modify NaElement object + :param na_element_object: modify or create NaElement object + :param values: dictionary of cron values to be added + :return: None + """ + for key in values: + if key in self.na_helper.zapi_string_keys: + zapi_key = self.na_helper.zapi_string_keys.get(key) + na_element_object[zapi_key] = values[key] + elif key in self.na_helper.zapi_list_keys: + parent_key, child_key = self.na_helper.zapi_list_keys.get(key) + na_element_object.add_child_elem(self.na_helper.get_value_for_list(from_zapi=False, + zapi_parent=parent_key, + zapi_child=child_key, + data=values[key])) + elif key in self.na_helper.zapi_int_keys: + zapi_key = self.na_helper.zapi_int_keys.get(key) + na_element_object[zapi_key] = self.na_helper.get_value_for_int(from_zapi=False, + value=values[key]) + elif key in self.na_helper.zapi_bool_keys: + zapi_key = self.na_helper.zapi_bool_keys.get(key) + na_element_object[zapi_key] = self.na_helper.get_value_for_bool(from_zapi=False, + value=values[key]) def create_export_policy_rule(self): """ create rule for the export policy. """ - options = {'policy-name': self.policy_name, - 'client-match': self.client_match} - if self.allow_suid is not None: - options['is-allow-set-uid-enabled'] = 'true' if self.allow_suid else 'false' - if self.rule_index is not None: - options['rule-index'] = str(self.rule_index) - export_rule_create = netapp_utils.zapi.NaElement.create_node_with_children( - 'export-rule-create', **options) - export_rule_create.add_node_with_children( - 'ro-rule', **{'security-flavor': self.ro_rule}) - export_rule_create.add_node_with_children( - 'rw-rule', **{'security-flavor': self.rw_rule}) - if self.protocol: - export_rule_create.add_node_with_children( - 'protocol', **{'access-protocol': self.protocol}) - if self.super_user_security: - export_rule_create.add_node_with_children( - 'super-user-security', **{'security-flavor': self.super_user_security}) - + for key in ['client_match', 'ro_rule', 'rw_rule']: + if self.parameters.get(key) is None: + self.module.fail_json(msg='Error: Missing required param for creating export policy rule %s' % key) + export_rule_create = netapp_utils.zapi.NaElement('export-rule-create') + self.add_parameters_for_create_or_modify(export_rule_create, self.parameters) try: - self.server.invoke_successfully(export_rule_create, - enable_tunneling=True) + self.server.invoke_successfully(export_rule_create, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error creating export policy rule %s: %s' - % (self.policy_name, to_native(error)), - exception=traceback.format_exc()) + % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def create_export_policy(self): """ Creates an export policy """ export_policy_create = netapp_utils.zapi.NaElement.create_node_with_children( - 'export-policy-create', **{'policy-name': self.policy_name}) + 'export-policy-create', **{'policy-name': self.parameters['name']}) try: self.server.invoke_successfully(export_policy_create, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error creating export-policy %s: %s' - % (self.policy_name, to_native(error)), + % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def delete_export_policy_rule(self, rule_index): """ delete rule for the export policy. """ + if self.parameters.get('rule_index') is None: + self.parameters['rule_index'] = rule_index export_rule_delete = netapp_utils.zapi.NaElement.create_node_with_children( - 'export-rule-destroy', **{'policy-name': self.policy_name, + 'export-rule-destroy', **{'policy-name': self.parameters['name'], 'rule-index': str(rule_index)}) try: @@ -352,185 +369,58 @@ class NetAppontapExportRule(object): enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error deleting export policy rule %s: %s' - % (self.policy_name, to_native(error)), + % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) - def modify_protocol(self, rule_index): - """ - Modify the protocol. - """ - export_rule_modify_protocol = netapp_utils.zapi.NaElement.create_node_with_children( - 'export-rule-modify', - **{'policy-name': self.policy_name, - 'rule-index': rule_index}) - export_rule_modify_protocol.add_node_with_children( - 'protocol', **{'access-protocol': self.protocol}) + def modify_export_policy_rule(self, params): + ''' + Modify an existing export policy rule + :param params: dict() of attributes with desired values + :return: None + ''' + export_rule_modify = netapp_utils.zapi.NaElement.create_node_with_children( + 'export-rule-modify', **{'policy-name': self.parameters['name'], + 'rule-index': str(self.parameters['rule_index'])}) + self.add_parameters_for_create_or_modify(export_rule_modify, params) try: - self.server.invoke_successfully(export_rule_modify_protocol, - enable_tunneling=True) + self.server.invoke_successfully(export_rule_modify, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: - self.module.fail_json(msg='Error modifying protocol %s: %s' % (self.policy_name, to_native(error)), + self.module.fail_json(msg='Error modifying allow_suid %s: %s' + % (self.parameters['allow_suid'], to_native(error)), exception=traceback.format_exc()) - def modify_ro_rule(self, rule_index): - """ - Modify ro_rule. - """ - export_rule_modify_ro_rule = netapp_utils.zapi.NaElement.create_node_with_children( - 'export-rule-modify', - **{'policy-name': self.policy_name, - 'rule-index': rule_index}) - export_rule_modify_ro_rule.add_node_with_children( - 'ro-rule', **{'security-flavor': self.ro_rule}) - try: - self.server.invoke_successfully(export_rule_modify_ro_rule, - enable_tunneling=True) - except netapp_utils.zapi.NaApiError as error: - self.module.fail_json(msg='Error modifying ro_rule %s: %s' % (self.ro_rule, to_native(error)), - exception=traceback.format_exc()) - - def modify_rw_rule(self, rule_index): - """ - Modify rw_rule. - """ - export_rule_modify_rw_rule = netapp_utils.zapi.NaElement.create_node_with_children( - 'export-rule-modify', - **{'policy-name': self.policy_name, - 'rule-index': rule_index}) - export_rule_modify_rw_rule.add_node_with_children( - 'rw-rule', **{'security-flavor': self.rw_rule}) - try: - self.server.invoke_successfully(export_rule_modify_rw_rule, - enable_tunneling=True) - except netapp_utils.zapi.NaApiError as error: - self.module.fail_json(msg='Error modifying rw_rule %s: %s' % (self.rw_rule, to_native(error)), - exception=traceback.format_exc()) - - def modify_super_user_security(self, rule_index): - """ - Modify super_user_security. - """ - export_rule_modify_super_user_security = netapp_utils.zapi.NaElement.create_node_with_children( - 'export-rule-modify', - **{'policy-name': self.policy_name, - 'rule-index': rule_index}) - export_rule_modify_super_user_security.add_node_with_children( - 'super-user-security', **{'security-flavor': self.super_user_security}) - try: - self.server.invoke_successfully(export_rule_modify_super_user_security, - enable_tunneling=True) - except netapp_utils.zapi.NaApiError as error: - self.module.fail_json(msg='Error modifying super_user_security %s: %s' % (self.super_user_security, to_native(error)), - exception=traceback.format_exc()) - - def modify_client_match(self, rule_index): - """ - Modify client_match. - """ - export_rule_modify_client_match = netapp_utils.zapi.NaElement.create_node_with_children( - 'export-rule-modify', - **{'policy-name': self.policy_name, - 'rule-index': rule_index, - 'client-match': str(self.client_match)}) - try: - self.server.invoke_successfully(export_rule_modify_client_match, - enable_tunneling=True) - except netapp_utils.zapi.NaApiError as error: - self.module.fail_json(msg='Error modifying client_match %s: %s' % (self.client_match, to_native(error)), - exception=traceback.format_exc()) - - def modify_allow_suid(self, rule_index): - """ - Modify allow_suid. - """ - export_rule_modify_allow_suid = netapp_utils.zapi.NaElement.create_node_with_children( - 'export-rule-modify', - **{'policy-name': self.policy_name, - 'rule-index': rule_index, - 'is-allow-set-uid-enabled': 'true' if self.allow_suid else 'false'}) - try: - self.server.invoke_successfully(export_rule_modify_allow_suid, - enable_tunneling=True) - except netapp_utils.zapi.NaApiError as error: - self.module.fail_json(msg='Error modifying allow_suid %s: %s' % (self.allow_suid, to_native(error)), - exception=traceback.format_exc()) + def autosupport_log(self): + netapp_utils.ems_log_event("na_ontap_export_policy_rules", self.server) def apply(self): - '''Apply action to export-policy''' - changed = False - export_policy_rule_exists = None - export_rule_protocol_changed = False - export_rule_ro_rule_changed = False - export_rule_rw_rule_changed = False - export_rule_allow_suid_enabled = False - export_rule_clientmatch_changed = False - export_rule_superuser_changed = False - netapp_utils.ems_log_event("na_ontap_export_policy_rules", self.server) - export_policy_details = self.get_export_policy() + ''' Apply required action from the play''' + self.autosupport_log() + # convert client_match list to comma-separated string + if self.parameters.get('client_match') is not None: + self.parameters['client_match'] = ','.join(self.parameters['client_match']) - if not export_policy_details: - if self.state == 'present': - self.create_export_policy() - export_policy_rule_exists = self.get_export_policy_rule() - if self.state == 'absent': - if export_policy_rule_exists: # delete - changed = True - rule_index = export_policy_rule_exists['rule-index'] + current, modify = self.get_export_policy_rule(), None + action = self.na_helper.get_cd_action(current, self.parameters) + if action is None and self.parameters['state'] == 'present': + modify = self.na_helper.get_modified_attributes(current, self.parameters) - elif self.state == 'present': - if export_policy_rule_exists: # modify - rule_index = export_policy_rule_exists['rule-index'] - if rule_index: - if self.protocol is not None and \ - export_policy_rule_exists['protocol'] != self.protocol: - export_rule_protocol_changed = True - changed = True - if self.ro_rule is not None and \ - export_policy_rule_exists['ro-rule'] != self.ro_rule: - export_rule_ro_rule_changed = True - changed = True - if self.rw_rule is not None and \ - export_policy_rule_exists['rw-rule'] != self.rw_rule: - export_rule_rw_rule_changed = True - changed = True - if self.allow_suid is not None and \ - export_policy_rule_exists['is-allow-set-uid-enabled'] != self.allow_suid: - export_rule_allow_suid_enabled = True - changed = True - if self.super_user_security is not None and \ - export_policy_rule_exists['super-user-security'] != self.super_user_security: - export_rule_superuser_changed = True - changed = True - if self.client_match is not None and \ - export_policy_rule_exists['client-match'] != self.client_match: - export_rule_clientmatch_changed = True - changed = True - else: # create - changed = True - if changed: + if self.na_helper.changed: if self.module.check_mode: pass else: - if self.state == 'present': - if not export_policy_rule_exists: - self.create_export_policy_rule() - else: - if export_rule_protocol_changed: - self.modify_protocol(rule_index) - if export_rule_ro_rule_changed: - self.modify_ro_rule(rule_index) - if export_rule_rw_rule_changed: - self.modify_rw_rule(rule_index) - if export_rule_allow_suid_enabled: - self.modify_allow_suid(rule_index) - if export_rule_clientmatch_changed: - self.modify_client_match(rule_index) - if export_rule_superuser_changed: - self.modify_super_user_security(rule_index) - elif self.state == 'absent': - self.delete_export_policy_rule(rule_index) - - self.module.exit_json(changed=changed) + # create export policy (if policy doesn't exist) only when changed=True + if not self.get_export_policy(): + self.create_export_policy() + if action == 'create': + self.create_export_policy_rule() + elif action == 'delete': + if current['num_records'] > 1: + self.module.fail_json(msg='Multiple export policy rules exist.' + 'Please specify a rule_index to delete') + self.delete_export_policy_rule(current['rule_index']) + elif modify: + self.modify_export_policy_rule(modify) + self.module.exit_json(changed=self.na_helper.changed) def main(): diff --git a/test/units/modules/storage/netapp/test_na_ontap_export_policy_rule.py b/test/units/modules/storage/netapp/test_na_ontap_export_policy_rule.py new file mode 100644 index 0000000000..5874603c97 --- /dev/null +++ b/test/units/modules/storage/netapp/test_na_ontap_export_policy_rule.py @@ -0,0 +1,264 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import print_function +import json +import pytest + +from units.compat import unittest +from units.compat.mock import patch, Mock +from ansible.module_utils import basic +from ansible.module_utils._text import to_bytes +import ansible.module_utils.netapp as netapp_utils + +from ansible.modules.storage.netapp.na_ontap_export_policy_rule \ + import NetAppontapExportRule as policy_rule # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +def set_module_args(args): + """prepare arguments so that they will be picked up during module creation""" + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access + + +class AnsibleExitJson(Exception): + """Exception class to be raised by module.exit_json and caught by the test case""" + pass + + +class AnsibleFailJson(Exception): + """Exception class to be raised by module.fail_json and caught by the test case""" + pass + + +def exit_json(*args, **kwargs): # pylint: disable=unused-argument + """function to patch over exit_json; package return data into an exception""" + if 'changed' not in kwargs: + kwargs['changed'] = False + raise AnsibleExitJson(kwargs) + + +def fail_json(*args, **kwargs): # pylint: disable=unused-argument + """function to patch over fail_json; package return data into an exception""" + kwargs['failed'] = True + raise AnsibleFailJson(kwargs) + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.kind = kind + self.data = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.kind == 'rule': + xml = self.build_policy_rule(self.data) + if self.kind == 'rules': + xml = self.build_policy_rule(self.data, multiple=True) + if self.kind == 'policy': + xml = self.build_policy() + self.xml_out = xml + return xml + + @staticmethod + def build_policy_rule(policy, multiple=False): + ''' build xml data for vserser-info ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = {'attributes-list': { + 'export-rule-info': { + 'policy-name': policy['name'], + 'client-match': policy['client_match'], + 'ro-rule': { + 'security-flavor': 'any' + }, + 'rw-rule': { + 'security-flavor': 'any' + }, + 'protocol': { + 'access-protocol': policy['protocol'] + }, + 'super-user-security': { + 'security-flavor': 'any' + }, + 'is-allow-set-uid-enabled': 'false', + 'rule-index': policy['rule_index'] + } + }, 'num-records': 2 if multiple is True else 1} + xml.translate_struct(attributes) + return xml + + @staticmethod + def build_policy(): + ''' build xml data for export-policy-get-iter ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'num-records': 1, + + } + xml.translate_struct(attributes) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.mock_module_helper = patch.multiple(basic.AnsibleModule, + exit_json=exit_json, + fail_json=fail_json) + self.mock_module_helper.start() + self.addCleanup(self.mock_module_helper.stop) + self.server = MockONTAPConnection() + self.mock_rule = { + 'name': 'test', + 'protocol': 'nfs', + 'client_match': '1.1.1.0', + 'rule_index': 10 + } + + def mock_rule_args(self): + return { + 'name': self.mock_rule['name'], + 'client_match': self.mock_rule['client_match'], + 'vserver': 'test', + 'protocol': self.mock_rule['protocol'], + 'rule_index': self.mock_rule['rule_index'], + 'ro_rule': 'any', + 'rw_rule': 'any', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!' + } + + def get_mock_object(self, kind=None): + """ + Helper method to return an na_ontap_firewall_policy object + :param kind: passes this param to MockONTAPConnection() + :return: na_ontap_firewall_policy object + """ + obj = policy_rule() + obj.autosupport_log = Mock(return_value=None) + if kind is None: + obj.server = MockONTAPConnection() + else: + obj.server = MockONTAPConnection(kind=kind, data=self.mock_rule_args()) + return obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + policy_rule() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_get_nonexistent_rule(self): + ''' Test if get_export_policy_rule returns None for non-existent policy ''' + set_module_args(self.mock_rule_args()) + result = self.get_mock_object().get_export_policy_rule() + assert result is None + + def test_get_nonexistent_policy(self): + ''' Test if get_export_policy returns None for non-existent policy ''' + set_module_args(self.mock_rule_args()) + result = self.get_mock_object().get_export_policy() + assert result is None + + def test_get_existing_rule(self): + ''' Test if get_export_policy_rule returns rule details for existing policy ''' + data = self.mock_rule_args() + set_module_args(data) + result = self.get_mock_object('rule').get_export_policy_rule() + assert result['name'] == data['name'] + assert result['client_match'] == data['client_match'] + assert result['ro_rule'] == ['any'] # from build_rule() + + def test_get_existing_policy(self): + ''' Test if get_export_policy returns policy details for existing policy ''' + data = self.mock_rule_args() + set_module_args(data) + result = self.get_mock_object('policy').get_export_policy() + assert result is not None + + def test_create_missing_param_error(self): + ''' Test validation error from create ''' + data = self.mock_rule_args() + del data['ro_rule'] + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_mock_object().apply() + msg = 'Error: Missing required param for creating export policy rule ro_rule' + assert exc.value.args[0]['msg'] == msg + + def test_successful_create(self): + ''' Test successful create ''' + set_module_args(self.mock_rule_args()) + with pytest.raises(AnsibleExitJson) as exc: + self.get_mock_object().apply() + assert exc.value.args[0]['changed'] + + def test_create_idempotency(self): + ''' Test create idempotency ''' + set_module_args(self.mock_rule_args()) + with pytest.raises(AnsibleExitJson) as exc: + self.get_mock_object('rule').apply() + assert not exc.value.args[0]['changed'] + + def test_successful_delete_without_rule_index(self): + ''' Test delete existing job ''' + data = self.mock_rule_args() + data['state'] = 'absent' + del data['rule_index'] + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_mock_object('rule').apply() + assert exc.value.args[0]['changed'] + + def test_delete_idempotency(self): + ''' Test delete idempotency ''' + data = self.mock_rule_args() + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_mock_object().apply() + assert not exc.value.args[0]['changed'] + + def test_successful_modify(self): + ''' Test successful modify protocol ''' + data = self.mock_rule_args() + data['protocol'] = ['cifs'] + data['allow_suid'] = 'true' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_mock_object('rule').apply() + assert exc.value.args[0]['changed'] + + def test_error_on_ambiguous_delete(self): + ''' Test error if multiple entries match for a delete ''' + data = self.mock_rule_args() + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_mock_object('rules').apply() + msg = "Multiple export policy rules exist.Please specify a rule_index to delete" + assert exc.value.args[0]['msg'] == msg + + def test_helper_query_parameters(self): + ''' Test helper method set_query_parameters() ''' + data = self.mock_rule_args() + set_module_args(data) + result = self.get_mock_object('rule').set_query_parameters() + print(str(result)) + assert 'query' in result + assert 'export-rule-info' in result['query'] + assert result['query']['export-rule-info']['rule-index'] == data['rule_index']