Server IP : 85.214.239.14 / Your IP : 52.14.7.103 Web Server : Apache/2.4.62 (Debian) System : Linux h2886529.stratoserver.net 4.9.0 #1 SMP Tue Jan 9 19:45:01 MSK 2024 x86_64 User : www-data ( 33) PHP Version : 7.4.18 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare, MySQL : OFF | cURL : OFF | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /lib/python3/dist-packages/ansible_collections/netapp/ontap/plugins/modules/ |
Upload File : |
#!/usr/bin/python # (c) 2018-2023, NetApp, Inc # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ''' na_ontap_user ''' from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = ''' module: na_ontap_user short_description: NetApp ONTAP user configuration and management extends_documentation_fragment: - netapp.ontap.netapp.na_ontap version_added: 2.6.0 author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com> description: - Create or destroy users. options: state: description: - Whether the specified user should exist or not. choices: ['present', 'absent'] type: str default: 'present' name: description: - The name of the user to manage. required: true type: str application_strs: version_added: 21.6.0 description: - List of applications to grant access to. - This option maintains backward compatibility with the existing C(applications) option, but is limited. - It is recommended to use the new C(application_dicts) option which provides more flexibility. - Creating a login with application console, telnet, rsh, and service-processor for a data vserver is not supported. - Module supports both service-processor and service_processor choices. - ZAPI requires service-processor, while REST requires service_processor, except for an issue with ONTAP 9.6 and 9.7. - snmp is not supported in REST. - Either C(application_dicts) or C(application_strs) is required. type: list elements: str choices: ['console', 'http','ontapi','rsh','snmp','service_processor','service-processor','sp','ssh','telnet'] aliases: - application - applications application_dicts: version_added: 21.6.0 description: - List of applications to grant access to. Provides better control on applications and authentication methods. - Creating a login with application console, telnet, rsh, and service-processor for a data vserver is not supported. - Module supports both service-processor and service_processor choices. - ZAPI requires service-processor, while REST requires service_processor, except for an issue with ONTAP 9.6 and 9.7. - snmp is not supported in REST. - Either C(application_dicts) or C(application_strs) is required. type: list elements: dict suboptions: application: description: name of the application. type: str choices: ['console', 'http','ontapi','rsh','snmp','service_processor','service-processor','sp','ssh','telnet'] required: true authentication_methods: description: list of authentication methods for the application (see C(authentication_method)). type: list elements: str choices: ['community', 'password', 'publickey', 'domain', 'nsswitch', 'usm', 'cert', 'saml'] required: true second_authentication_method: description: when using ssh, optional additional authentication method for MFA. type: str choices: ['none', 'password', 'publickey', 'nsswitch'] authentication_method: description: - Authentication method for the application. If you need more than one method, use C(application_dicts). - Not all authentication methods are valid for an application. - Valid authentication methods for each application are as denoted in I(authentication_choices_description). - Password for console application - Password, domain, nsswitch, cert, saml for http application. - Password, domain, nsswitch, cert, saml for ontapi application. - SAML is only supported with REST, but seems to work with ZAPI as well. - Community for snmp application (when creating SNMPv1 and SNMPv2 users). - The usm and community for snmp application (when creating SNMPv3 users). - Password for sp application. - Password for rsh application. - Password for telnet application. - Password, publickey, domain, nsswitch for ssh application. - Required when C(application_strs) is present. type: str choices: ['community', 'password', 'publickey', 'domain', 'nsswitch', 'usm', 'cert', 'saml'] set_password: description: - Password for the user account. - It is ignored for creating snmp users, but is required for creating non-snmp users. - For an existing user, this value will be used as the new password. type: str role_name: description: - The name of the role. Required when C(state=present) type: str lock_user: description: - Whether the specified user account is locked. type: bool vserver: description: - The name of the vserver to use. - Required with ZAPI. - With REST, ignore this option for creating cluster scoped interface. aliases: - svm type: str authentication_protocol: description: - Authentication protocol for the snmp user. - When cluster FIPS mode is on, 'sha' and 'sha2-256' are the only possible and valid values. - When cluster FIPS mode is off, the default value is 'none'. - When cluster FIPS mode is on, the default value is 'sha'. - Only available for 'usm' authentication method and non modifiable. choices: ['none', 'md5', 'sha', 'sha2-256'] type: str version_added: '20.6.0' authentication_password: description: - Password for the authentication protocol. This should be minimum 8 characters long. - This is required for 'md5', 'sha' and 'sha2-256' authentication protocols and not required for 'none'. - Only available for 'usm' authentication method and non modifiable. type: str version_added: '20.6.0' engine_id: description: - Authoritative entity's EngineID for the SNMPv3 user. - This should be specified as a hexadecimal string. - Engine ID with first bit set to 1 in first octet should have a minimum of 5 or maximum of 32 octets. - Engine Id with first bit set to 0 in the first octet should be 12 octets in length. - Engine Id cannot have all zeros in its address. - Only available for 'usm' authentication method and non modifiable. type: str version_added: '20.6.0' privacy_protocol: description: - Privacy protocol for the snmp user. - When cluster FIPS mode is on, 'aes128' is the only possible and valid value. - When cluster FIPS mode is off, the default value is 'none'. When cluster FIPS mode is on, the default value is 'aes128'. - Only available for 'usm' authentication method and non modifiable. choices: ['none', 'des', 'aes128'] type: str version_added: '20.6.0' privacy_password: description: - Password for the privacy protocol. This should be minimum 8 characters long. - This is required for 'des' and 'aes128' privacy protocols and not required for 'none'. - Only available for 'usm' authentication method and non modifiable. type: str version_added: '20.6.0' remote_switch_ipaddress: description: - This optionally specifies the IP Address of the remote switch. - The remote switch could be a cluster switch monitored by Cluster Switch Health Monitor (CSHM) or a Fiber Channel (FC) switch monitored by Metro Cluster Health Monitor (MCC-HM). - This is applicable only for a remote SNMPv3 user i.e. only if user is a remote (non-local) user, application is snmp and authentication method is usm. type: str version_added: '20.6.0' replace_existing_apps_and_methods: description: - If the user already exists, the current applications and authentications methods are replaced when state=present. - If the user already exists, the current applications and authentications methods are removed when state=absent. - When using application_dicts or REST, this the only supported behavior. - When using application_strs and ZAPI, this is the behavior when this option is set to always. - When using application_strs and ZAPI, if the option is set to auto, applications that are not listed are not removed. - When using application_strs and ZAPI, if the option is set to auto, authentication mehods that are not listed are not removed. - C(auto) preserve the existing behavior for backward compatibility, but note that REST and ZAPI have inconsistent behavior. - This is another reason to recommend to use C(application_dicts). type: str choices: ['always', 'auto'] default: 'auto' version_added: '20.6.0' ''' EXAMPLES = """ - name: Create User netapp.ontap.na_ontap_user: state: present name: SampleUser applications: ssh,console authentication_method: password set_password: apn1242183u1298u41 lock_user: True role_name: vsadmin vserver: ansibleVServer hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" - name: Create cluster scoped user in REST. netapp.ontap.na_ontap_user: state: present name: SampleUser applications: ssh,console authentication_method: password set_password: apn1242183u1298u41 lock_user: True role_name: admin hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" - name: Delete User netapp.ontap.na_ontap_user: state: absent name: SampleUser applications: ssh authentication_method: password vserver: ansibleVServer hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" password: "{{ netapp_password }}" - name: Create user with snmp application (ZAPI) netapp.ontap.na_ontap_user: state: present name: test_cert_snmp applications: snmp authentication_method: usm role_name: admin authentication_protocol: md5 authentication_password: '12345678' privacy_protocol: 'aes128' privacy_password: '12345678' engine_id: '7063514941000000000000' remote_switch_ipaddress: 10.0.0.0 vserver: "{{ vserver }}" hostname: "{{ hostname }}" username: "{{ username }}" password: "{{ password }}" - name: Create user netapp.ontap.na_ontap_user: state: present name: test123 application_dicts: - application: http authentication_methods: password - application: ssh authentication_methods: password,publickey role_name: vsadmin set_password: bobdole1234566 vserver: "{{ vserver }}" hostname: "{{ hostname }}" username: "{{ username }}" password: "{{ password }}" """ RETURN = """ """ import traceback from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_native import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule class NetAppOntapUser: """ Common operations to manage users and roles. """ def __init__(self): self.use_rest = False self.argument_spec = netapp_utils.na_ontap_host_argument_spec() self.argument_spec.update(dict( state=dict(type='str', choices=['present', 'absent'], default='present'), name=dict(required=True, type='str'), application_strs=dict(type='list', elements='str', aliases=['application', 'applications'], choices=['console', 'http', 'ontapi', 'rsh', 'snmp', 'sp', 'service-processor', 'service_processor', 'ssh', 'telnet'],), application_dicts=dict(type='list', elements='dict', options=dict( application=dict(required=True, type='str', choices=['console', 'http', 'ontapi', 'rsh', 'snmp', 'sp', 'service-processor', 'service_processor', 'ssh', 'telnet'],), authentication_methods=dict(required=True, type='list', elements='str', choices=['community', 'password', 'publickey', 'domain', 'nsswitch', 'usm', 'cert', 'saml']), second_authentication_method=dict(type='str', choices=['none', 'password', 'publickey', 'nsswitch']))), authentication_method=dict(type='str', choices=['community', 'password', 'publickey', 'domain', 'nsswitch', 'usm', 'cert', 'saml']), set_password=dict(type='str', no_log=True), role_name=dict(type='str'), lock_user=dict(type='bool'), vserver=dict(type='str', aliases=['svm']), authentication_protocol=dict(type='str', choices=['none', 'md5', 'sha', 'sha2-256']), authentication_password=dict(type='str', no_log=True), engine_id=dict(type='str'), privacy_protocol=dict(type='str', choices=['none', 'des', 'aes128']), privacy_password=dict(type='str', no_log=True), remote_switch_ipaddress=dict(type='str'), replace_existing_apps_and_methods=dict(type='str', choices=['always', 'auto'], default='auto') )) self.module = AnsibleModule( argument_spec=self.argument_spec, mutually_exclusive=[ ('application_strs', 'application_dicts') ], required_together=[ ('application_strs', 'authentication_method') ], supports_check_mode=True ) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) self.strs_to_dicts() # REST API should be used for ONTAP 9.6 or higher self.rest_api = netapp_utils.OntapRestAPI(self.module) # some attributes are not supported in earlier REST implementation unsupported_rest_properties = ['authentication_password', 'authentication_protocol', 'engine_id', 'privacy_password', 'privacy_protocol'] self.use_rest = self.rest_api.is_rest_supported_properties(self.parameters, unsupported_rest_properties) if not self.use_rest: if self.parameters.get('vserver') is None: self.module.fail_json(msg="Error: vserver is required with ZAPI") if not netapp_utils.has_netapp_lib(): self.module.fail_json(msg=netapp_utils.netapp_lib_is_required()) self.server = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=self.parameters['vserver']) self.validate_applications() def validate_applications(self): if not self.use_rest: if self.parameters['applications'] is None: self.module.fail_json(msg="application_dicts or application_strs is a required parameter with ZAPI") for application in self.parameters['applications']: if application['application'] == 'service_processor': application['application'] = 'service-processor' if self.parameters['applications'] is None: return application_keys = [] for application in self.parameters['applications']: # make sure app entries are not duplicated application_name = application['application'] if application_name in application_keys: self.module.fail_json(msg='Error: repeated application name: %s. Group all authentication methods under a single entry.' % application_name) application_keys.append(application_name) if self.use_rest: if application_name == 'snmp': self.module.fail_json(msg="snmp as application is not supported in REST.") # REST prefers certificate to cert application['authentication_methods'] = ['certificate' if x == 'cert' else x for x in application['authentication_methods']] # REST get always returns 'second_authentication_method' if 'second_authentication_method' not in application: application['second_authentication_method'] = None def strs_to_dicts(self): """transform applications list of strs to a list of dicts if application_strs in use""" if 'application_dicts' in self.parameters: for application in self.parameters['application_dicts']: # keep them sorted for comparison with current application['authentication_methods'].sort() self.parameters['applications'] = self.parameters['application_dicts'] self.parameters['replace_existing_apps_and_methods'] = 'always' elif 'application_strs' in self.parameters: # actual conversion self.parameters['applications'] = [ dict(application=application, authentication_methods=[self.parameters['authentication_method']], second_authentication_method=None ) for application in self.parameters['application_strs']] else: self.parameters['applications'] = None def get_user_rest(self): api = 'security/accounts' query = { 'name': self.parameters['name'] } if self.parameters.get('vserver') is None: # vserser is empty for cluster query['scope'] = 'cluster' else: query['owner.name'] = self.parameters['vserver'] message, error = self.rest_api.get(api, query) if error: self.module.fail_json(msg='Error while fetching user info: %s' % error) if message['num_records'] == 1: return message['records'][0]['owner']['uuid'], message['records'][0]['name'] if message['num_records'] > 1: self.module.fail_json(msg='Error while fetching user info, found multiple entries: %s' % repr(message)) return None def get_user_details_rest(self, name, owner_uuid): query = { 'fields': 'role,applications,locked' } api = "security/accounts/%s/%s" % (owner_uuid, name) response, error = self.rest_api.get(api, query) if error: self.module.fail_json(msg='Error while fetching user details: %s' % error) if response: # replace "none" values with None for comparison for application in response['applications']: if application.get('second_authentication_method') == 'none': application['second_authentication_method'] = None # new read-only attribute in 9.11, breaks idempotency when present application.pop('is_ldap_fastbind', None) return_value = { 'role_name': response['role']['name'], 'applications': response['applications'] } if "locked" in response: return_value['lock_user'] = response['locked'] return return_value def get_user(self): """ Checks if the user exists. :param: application: application to grant access to, a dict :return: Dictionary if user found None if user is not found """ desired_applications = [application['application'] for application in self.parameters['applications']] desired_method = self.parameters.get('authentication_method') security_login_get_iter = netapp_utils.zapi.NaElement('security-login-get-iter') query_details = netapp_utils.zapi.NaElement.create_node_with_children( 'security-login-account-info', **{'vserver': self.parameters['vserver'], 'user-name': self.parameters['name']}) query = netapp_utils.zapi.NaElement('query') query.add_child_elem(query_details) security_login_get_iter.add_child_elem(query) try: result = self.server.invoke_successfully(security_login_get_iter, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: if to_native(error.code) in ['16034', '16043']: # Error 16034 denotes a user not being found. # Error 16043 denotes the user existing, but the application missing. return None self.module.fail_json(msg='Error getting user %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) if not result.get_child_by_name('num-records') or not int(result.get_child_content('num-records')): return None applications = {} attr = result.get_child_by_name('attributes-list') locks = [] for info in attr.get_children(): lock_user = self.na_helper.get_value_for_bool(True, info.get_child_content('is-locked')) locks.append(lock_user) role_name = info.get_child_content('role-name') application = info.get_child_content('application') auth_method = info.get_child_content('authentication-method') sec_method = info.get_child_content('second-authentication-method') if self.parameters['replace_existing_apps_and_methods'] == 'always' and application in applications: applications[application][0].append(auth_method) if sec_method != 'none': # we can't change sec_method in place, a tuple is not mutable applications[application] = (applications[application][0], sec_method) elif (self.parameters['replace_existing_apps_and_methods'] == 'always' or (application in desired_applications and auth_method == desired_method)): # with 'auto' we ignore existing apps that were not asked for # with auto, only a single method is supported applications[application] = ([auth_method], sec_method if sec_method != 'none' else None) apps = [dict(application=application, authentication_methods=sorted(methods), second_authentication_method=sec_method) for application, (methods, sec_method) in applications.items()] return dict( lock_user=any(locks), role_name=role_name, applications=apps ) def create_user_rest(self, apps): api = 'security/accounts' body = { 'name': self.parameters['name'], 'role.name': self.parameters['role_name'], 'applications': self.na_helper.filter_out_none_entries(apps) } if self.parameters.get('vserver') is not None: # vserser is empty for cluster body['owner.name'] = self.parameters['vserver'] if 'set_password' in self.parameters: body['password'] = self.parameters['set_password'] if 'lock_user' in self.parameters: body['locked'] = self.parameters['lock_user'] dummy, error = self.rest_api.post(api, body) if ( error and 'invalid value' in error['message'] and any(x in error['message'] for x in ['service-processor', 'service_processor']) ): # find if there is an error for service processor application value # update value as per ONTAP version support app_list_sp = body['applications'] for app_item in app_list_sp: if app_item['application'] == 'service-processor': app_item['application'] = 'service_processor' elif app_item['application'] == 'service_processor': app_item['application'] = 'service-processor' body['applications'] = app_list_sp # post again and throw first error in case of an error dummy, error_sp = self.rest_api.post(api, body) if not error_sp: return # non-sp errors thrown or initial sp errors if error: self.module.fail_json(msg='Error while creating user: %s' % error) def create_user(self, application): for index in range(len(application['authentication_methods'])): self.create_user_with_auth(application, index) def create_user_with_auth(self, application, index): """ creates the user for the given application and authentication_method application is now a directory :param: application: application to grant access to """ user_create = netapp_utils.zapi.NaElement.create_node_with_children( 'security-login-create', **{'vserver': self.parameters['vserver'], 'user-name': self.parameters['name'], 'application': application['application'], 'authentication-method': application['authentication_methods'][index], 'role-name': self.parameters.get('role_name')}) if application.get('second_authentication_method') is not None: user_create.add_new_child('second-authentication-method', application['second_authentication_method']) if self.parameters.get('set_password') is not None: user_create.add_new_child('password', self.parameters.get('set_password')) if application['authentication_methods'][0] == 'usm': if self.parameters.get('remote_switch_ipaddress') is not None: user_create.add_new_child('remote-switch-ipaddress', self.parameters.get('remote_switch_ipaddress')) snmpv3_login_info = netapp_utils.zapi.NaElement('snmpv3-login-info') if self.parameters.get('authentication_password') is not None: snmpv3_login_info.add_new_child('authentication-password', self.parameters['authentication_password']) if self.parameters.get('authentication_protocol') is not None: snmpv3_login_info.add_new_child('authentication-protocol', self.parameters['authentication_protocol']) if self.parameters.get('engine_id') is not None: snmpv3_login_info.add_new_child('engine-id', self.parameters['engine_id']) if self.parameters.get('privacy_password') is not None: snmpv3_login_info.add_new_child('privacy-password', self.parameters['privacy_password']) if self.parameters.get('privacy_protocol') is not None: snmpv3_login_info.add_new_child('privacy-protocol', self.parameters['privacy_protocol']) user_create.add_child_elem(snmpv3_login_info) try: self.server.invoke_successfully(user_create, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error creating user %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def lock_unlock_user_rest(self, owner_uuid, username, value=None): body = { 'locked': value } error = self.patch_account(owner_uuid, username, body) if error: self.module.fail_json(msg='Error while locking/unlocking user: %s' % error) def lock_given_user(self): """ locks the user """ user_lock = netapp_utils.zapi.NaElement.create_node_with_children( 'security-login-lock', **{'vserver': self.parameters['vserver'], 'user-name': self.parameters['name']}) try: self.server.invoke_successfully(user_lock, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error locking user %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def unlock_given_user(self): """ unlocks the user """ user_unlock = netapp_utils.zapi.NaElement.create_node_with_children( 'security-login-unlock', **{'vserver': self.parameters['vserver'], 'user-name': self.parameters['name']}) try: self.server.invoke_successfully(user_unlock, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: if to_native(error.code) != '13114': self.module.fail_json(msg='Error unlocking user %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) return def delete_user_rest(self, owner_uuid, username): api = "security/accounts/%s/%s" % (owner_uuid, username) dummy, error = self.rest_api.delete(api) if error: self.module.fail_json(msg='Error while deleting user: %s' % error) def delete_user(self, application, methods_to_keep=None): for index, method in enumerate(application['authentication_methods']): if methods_to_keep is None or method not in methods_to_keep: self.delete_user_with_auth(application, index) def delete_user_with_auth(self, application, index): """ deletes the user for the given application and authentication_method application is now a dict :param: application: application to grant access to """ user_delete = netapp_utils.zapi.NaElement.create_node_with_children( 'security-login-delete', **{'vserver': self.parameters['vserver'], 'user-name': self.parameters['name'], 'application': application['application'], 'authentication-method': application['authentication_methods'][index]}) try: self.server.invoke_successfully(user_delete, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error removing user %s: %s - application: %s' % (self.parameters['name'], to_native(error), application), exception=traceback.format_exc()) @staticmethod def is_repeated_password(message): return message.startswith('New password must be different than last 6 passwords.') \ or message.startswith('New password must be different from last 6 passwords.') \ or message.startswith('New password must be different than the old password.') \ or message.startswith('New password must be different from the old password.') def change_password_rest(self, owner_uuid, username): body = { 'password': self.parameters['set_password'], } error = self.patch_account(owner_uuid, username, body) if error: if 'message' in error and self.is_repeated_password(error['message']): # if the password is reused, assume idempotency return False self.module.fail_json(msg='Error while updating user password: %s' % error) return True def change_password(self): """ Changes the password :return: True if password updated False if password is not updated :rtype: bool """ # self.server.set_vserver(self.parameters['vserver']) modify_password = netapp_utils.zapi.NaElement.create_node_with_children( 'security-login-modify-password', **{ 'new-password': str(self.parameters.get('set_password')), 'user-name': self.parameters['name']}) try: self.server.invoke_successfully(modify_password, enable_tunneling=True) except netapp_utils.zapi.NaApiError as error: if to_native(error.code) == '13114': return False # if the user give the same password, instead of returning an error, return ok if to_native(error.code) == '13214' and self.is_repeated_password(error.message): return False self.module.fail_json(msg='Error setting password for user %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) self.server.set_vserver(None) return True def modify_apps_rest(self, owner_uuid, username, apps=None): body = { 'role.name': self.parameters['role_name'], 'applications': self.na_helper.filter_out_none_entries(apps) } error = self.patch_account(owner_uuid, username, body) if error: self.module.fail_json(msg='Error while modifying user details: %s' % error) def patch_account(self, owner_uuid, username, body): query = {'name': self.parameters['name'], 'owner.uuid': owner_uuid} api = "security/accounts/%s/%s" % (owner_uuid, username) dummy, result = self.rest_api.patch(api, body, query) return result def modify_user(self, application, current_methods): for index, method in enumerate(application['authentication_methods']): if method in current_methods: self.modify_user_with_auth(application, index) else: self.create_user_with_auth(application, index) def modify_user_with_auth(self, application, index): """ Modify user application is now a dict """ user_modify = netapp_utils.zapi.NaElement.create_node_with_children( 'security-login-modify', **{'vserver': self.parameters['vserver'], 'user-name': self.parameters['name'], 'application': application['application'], 'authentication-method': application['authentication_methods'][index], 'role-name': self.parameters.get('role_name')}) try: self.server.invoke_successfully(user_modify, enable_tunneling=False) except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error modifying user %s: %s' % (self.parameters['name'], to_native(error)), exception=traceback.format_exc()) def change_sp_application(self, current_apps): """Adjust requested app name to match ONTAP convention""" if not self.parameters['applications']: return app_list = [app['application'] for app in current_apps] for application in self.parameters['applications']: if application['application'] == 'service_processor' and 'service-processor' in app_list: application['application'] = 'service-processor' elif application['application'] == 'service-processor' and 'service_processor' in app_list: application['application'] = 'service_processor' def validate_action(self, action): errors = [] if action == 'create': if not self.parameters.get('role_name'): errors.append('role_name') if not self.parameters.get('applications'): errors.append('application_dicts or application_strs') if errors: plural = 's' if len(errors) > 1 else '' self.module.fail_json(msg='Error: missing required parameter%s for %s: %s.' % (plural, action, ' and: '.join(errors))) def modify_apps_zapi(self, current, modify_decision): if 'applications' not in modify_decision: # to change roles, we need at least one app modify_decision['applications'] = self.parameters['applications'] current_apps = dict((application['application'], application['authentication_methods']) for application in current['applications']) for application in modify_decision['applications']: if application['application'] in current_apps: self.modify_user(application, current_apps[application['application']]) else: self.create_user(application) desired_apps = dict((application['application'], application['authentication_methods']) for application in self.parameters['applications']) for application in current['applications']: if application['application'] not in desired_apps: self.delete_user(application) else: self.delete_user(application, desired_apps[application['application']]) def get_current(self): owner_uuid, name = None, None if self.use_rest: current = self.get_user_rest() if current is not None: owner_uuid, name = current current = self.get_user_details_rest(name, owner_uuid) self.change_sp_application(current['applications']) else: current = self.get_user() return current, owner_uuid, name def define_actions(self, current): cd_action = self.na_helper.get_cd_action(current, self.parameters) modify = self.na_helper.get_modified_attributes(current, self.parameters) if cd_action is None else None if self.use_rest and cd_action is None and current and 'lock_user' not in current and self.parameters.get('lock_user') is not None: # REST does not return locked if password is not set if self.parameters.get('set_password') is None: self.module.fail_json(msg='Error: cannot modify lock state if password is not set.') modify['lock_user'] = self.parameters['lock_user'] self.na_helper.changed = True self.validate_action(cd_action) return cd_action, modify def take_action(self, cd_action, modify, current, owner_uuid, name): if cd_action == 'create': if self.use_rest: self.create_user_rest(self.parameters['applications']) else: for application in self.parameters['applications']: self.create_user(application) elif cd_action == 'delete': if self.use_rest: self.delete_user_rest(owner_uuid, name) else: for application in current['applications']: self.delete_user(application) elif modify: if 'role_name' in modify or 'applications' in modify: if self.use_rest: self.modify_apps_rest(owner_uuid, name, self.parameters['applications']) else: self.modify_apps_zapi(current, modify) return modify and 'lock_user' in modify def apply(self): current, owner_uuid, name = self.get_current() cd_action, modify = self.define_actions(current) deferred_lock = False if self.na_helper.changed and not self.module.check_mode: # lock/unlock actions require password to be set deferred_lock = self.take_action(cd_action, modify, current, owner_uuid, name) password_changed = False if cd_action is None and self.parameters.get('set_password') is not None and self.parameters['state'] == 'present': # if check_mode, don't attempt to change the password, but assume it would be changed if self.use_rest: password_changed = self.module.check_mode or self.change_password_rest(owner_uuid, name) else: password_changed = self.module.check_mode or self.change_password() if self.module.check_mode: self.module.warn('Module is not idempotent with check_mode when set_password is present.') if deferred_lock: if self.use_rest: self.lock_unlock_user_rest(owner_uuid, name, self.parameters['lock_user']) elif self.parameters.get('lock_user'): self.lock_given_user() else: self.unlock_given_user() self.module.exit_json(changed=self.na_helper.changed | password_changed, current=current, modify=modify) def main(): obj = NetAppOntapUser() obj.apply() if __name__ == '__main__': main()