1#!/usr/local/bin/python3.8 2 3# (c) 2018-2019, NetApp, Inc 4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 6''' 7na_ontap_user 8''' 9 10from __future__ import absolute_import, division, print_function 11__metaclass__ = type 12 13 14DOCUMENTATION = ''' 15 16module: na_ontap_user 17 18short_description: NetApp ONTAP user configuration and management 19extends_documentation_fragment: 20 - netapp.ontap.netapp.na_ontap 21version_added: 2.6.0 22author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com> 23 24description: 25- Create or destroy users. 26 27options: 28 state: 29 description: 30 - Whether the specified user should exist or not. 31 choices: ['present', 'absent'] 32 type: str 33 default: 'present' 34 name: 35 description: 36 - The name of the user to manage. 37 required: true 38 type: str 39 application_strs: 40 version_added: 21.6.0 41 description: 42 - List of applications to grant access to. 43 - This option maintains backward compatibility with the existing C(applications) option, but is limited. 44 - It is recommended to use the new C(application_dicts) option which provides more flexibility. 45 - Creating a login with application console, telnet, rsh, and service-processor for a data vserver is not supported. 46 - Module supports both service-processor and service_processor choices. 47 - ZAPI requires service-processor, while REST requires service_processor, except for an issue with ONTAP 9.6 and 9.7. 48 - snmp is not supported in REST. 49 - Either C(application_dicts) or C(application_strs) is required. 50 type: list 51 elements: str 52 choices: ['console', 'http','ontapi','rsh','snmp','service_processor','service-processor','sp','ssh','telnet'] 53 aliases: 54 - application 55 - applications 56 application_dicts: 57 version_added: 21.6.0 58 description: 59 - List of applications to grant access to. Provides better control on applications and authentication methods. 60 - Creating a login with application console, telnet, rsh, and service-processor for a data vserver is not supported. 61 - Module supports both service-processor and service_processor choices. 62 - ZAPI requires service-processor, while REST requires service_processor, except for an issue with ONTAP 9.6 and 9.7. 63 - snmp is not supported in REST. 64 - Either C(application_dicts) or C(application_strs) is required. 65 type: list 66 elements: dict 67 suboptions: 68 application: 69 description: name of the application. 70 type: str 71 choices: ['console', 'http','ontapi','rsh','snmp','service_processor','service-processor','sp','ssh','telnet'] 72 required: true 73 authentication_methods: 74 description: list of authentication methods for the application. 75 type: list 76 elements: str 77 choices: ['community', 'password', 'publickey', 'domain', 'nsswitch', 'usm', 'cert'] 78 required: true 79 second_authentication_method: 80 description: when using ssh, optional additional authentication method for MFA. 81 type: str 82 choices: ['none', 'password', 'publickey', 'nsswitch'] 83 authentication_method: 84 description: 85 - Authentication method for the application. If you need more than one method, use C(application_dicts). 86 - Not all authentication methods are valid for an application. 87 - Valid authentication methods for each application are as denoted in I(authentication_choices_description). 88 - Password for console application 89 - Password, domain, nsswitch, cert for http application. 90 - Password, domain, nsswitch, cert for ontapi application. 91 - Community for snmp application (when creating SNMPv1 and SNMPv2 users). 92 - The usm and community for snmp application (when creating SNMPv3 users). 93 - Password for sp application. 94 - Password for rsh application. 95 - Password for telnet application. 96 - Password, publickey, domain, nsswitch for ssh application. 97 - Required when C(application_strs) is present. 98 type: str 99 choices: ['community', 'password', 'publickey', 'domain', 'nsswitch', 'usm', 'cert'] 100 set_password: 101 description: 102 - Password for the user account. 103 - It is ignored for creating snmp users, but is required for creating non-snmp users. 104 - For an existing user, this value will be used as the new password. 105 type: str 106 role_name: 107 description: 108 - The name of the role. Required when C(state=present) 109 type: str 110 lock_user: 111 description: 112 - Whether the specified user account is locked. 113 type: bool 114 vserver: 115 description: 116 - The name of the vserver to use. 117 - With REST, for cluster scope, use a vserver entry with no value. 118 aliases: 119 - svm 120 required: true 121 type: str 122 authentication_protocol: 123 description: 124 - Authentication protocol for the snmp user. 125 - When cluster FIPS mode is on, 'sha' and 'sha2-256' are the only possible and valid values. 126 - When cluster FIPS mode is off, the default value is 'none'. 127 - When cluster FIPS mode is on, the default value is 'sha'. 128 - Only available for 'usm' authentication method and non modifiable. 129 choices: ['none', 'md5', 'sha', 'sha2-256'] 130 type: str 131 version_added: '20.6.0' 132 authentication_password: 133 description: 134 - Password for the authentication protocol. This should be minimum 8 characters long. 135 - This is required for 'md5', 'sha' and 'sha2-256' authentication protocols and not required for 'none'. 136 - Only available for 'usm' authentication method and non modifiable. 137 type: str 138 version_added: '20.6.0' 139 engine_id: 140 description: 141 - Authoritative entity's EngineID for the SNMPv3 user. 142 - This should be specified as a hexadecimal string. 143 - Engine ID with first bit set to 1 in first octet should have a minimum of 5 or maximum of 32 octets. 144 - Engine Id with first bit set to 0 in the first octet should be 12 octets in length. 145 - Engine Id cannot have all zeros in its address. 146 - Only available for 'usm' authentication method and non modifiable. 147 type: str 148 version_added: '20.6.0' 149 privacy_protocol: 150 description: 151 - Privacy protocol for the snmp user. 152 - When cluster FIPS mode is on, 'aes128' is the only possible and valid value. 153 - When cluster FIPS mode is off, the default value is 'none'. When cluster FIPS mode is on, the default value is 'aes128'. 154 - Only available for 'usm' authentication method and non modifiable. 155 choices: ['none', 'des', 'aes128'] 156 type: str 157 version_added: '20.6.0' 158 privacy_password: 159 description: 160 - Password for the privacy protocol. This should be minimum 8 characters long. 161 - This is required for 'des' and 'aes128' privacy protocols and not required for 'none'. 162 - Only available for 'usm' authentication method and non modifiable. 163 type: str 164 version_added: '20.6.0' 165 remote_switch_ipaddress: 166 description: 167 - This optionally specifies the IP Address of the remote switch. 168 - The remote switch could be a cluster switch monitored by Cluster Switch Health Monitor (CSHM) 169 or a Fiber Channel (FC) switch monitored by Metro Cluster Health Monitor (MCC-HM). 170 - This is applicable only for a remote SNMPv3 user i.e. only if user is a remote (non-local) user, 171 application is snmp and authentication method is usm. 172 type: str 173 version_added: '20.6.0' 174 replace_existing_apps_and_methods: 175 description: 176 - If the user already exists, the current applications and authentications methods are replaced when state=present. 177 - If the user already exists, the current applications and authentications methods are removed when state=absent. 178 - When using application_dicts or REST, this the only supported behavior. 179 - When using application_strs and ZAPI, this is the behavior when this option is set to always. 180 - When using application_strs and ZAPI, if the option is set to auto, applications that are not listed are not removed. 181 - When using application_strs and ZAPI, if the option is set to auto, authentication mehods that are not listed are not removed. 182 - C(auto) preserve the existing behavior for backward compatibility, but note that REST and ZAPI have inconsistent behavior. 183 - This is another reason to recommend to use C(application_dicts). 184 type: str 185 choices: ['always', 'auto'] 186 default: 'auto' 187 version_added: '20.6.0' 188''' 189 190EXAMPLES = """ 191 192 - name: Create User 193 netapp.ontap.na_ontap_user: 194 state: present 195 name: SampleUser 196 applications: ssh,console 197 authentication_method: password 198 set_password: apn1242183u1298u41 199 lock_user: True 200 role_name: vsadmin 201 vserver: ansibleVServer 202 hostname: "{{ netapp_hostname }}" 203 username: "{{ netapp_username }}" 204 password: "{{ netapp_password }}" 205 206 - name: Delete User 207 netapp.ontap.na_ontap_user: 208 state: absent 209 name: SampleUser 210 applications: ssh 211 authentication_method: password 212 vserver: ansibleVServer 213 hostname: "{{ netapp_hostname }}" 214 username: "{{ netapp_username }}" 215 password: "{{ netapp_password }}" 216 217 - name: Create user with snmp application (ZAPI) 218 netapp.ontap.na_ontap_user: 219 state: present 220 name: test_cert_snmp 221 applications: snmp 222 authentication_method: usm 223 role_name: admin 224 authentication_protocol: md5 225 authentication_password: '12345678' 226 privacy_protocol: 'aes128' 227 privacy_password: '12345678' 228 engine_id: '7063514941000000000000' 229 remote_switch_ipaddress: 10.0.0.0 230 vserver: "{{ vserver }}" 231 hostname: "{{ hostname }}" 232 username: "{{ username }}" 233 password: "{{ password }}" 234 235 - name: Create user 236 netapp.ontap.na_ontap_user: 237 state: present 238 name: test123 239 application_dicts: 240 - application: http 241 authentication_methods: password 242 - application: ssh 243 authentication_methods: password,publickey 244 role_name: vsadmin 245 set_password: bobdole1234566 246 vserver: "{{ vserver }}" 247 hostname: "{{ hostname }}" 248 username: "{{ username }}" 249 password: "{{ password }}" 250""" 251 252RETURN = """ 253 254""" 255import traceback 256 257from ansible.module_utils.basic import AnsibleModule 258from ansible.module_utils._text import to_native 259import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils 260from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule 261from ansible_collections.netapp.ontap.plugins.module_utils.netapp import OntapRestAPI 262 263HAS_NETAPP_LIB = netapp_utils.has_netapp_lib() 264 265 266class NetAppOntapUser(): 267 """ 268 Common operations to manage users and roles. 269 """ 270 271 def __init__(self): 272 self.use_rest = False 273 self.argument_spec = netapp_utils.na_ontap_host_argument_spec() 274 self.argument_spec.update(dict( 275 state=dict(type='str', choices=['present', 'absent'], default='present'), 276 name=dict(required=True, type='str'), 277 278 application_strs=dict(type='list', elements='str', aliases=['application', 'applications'], 279 choices=['console', 'http', 'ontapi', 'rsh', 'snmp', 280 'sp', 'service-processor', 'service_processor', 'ssh', 'telnet'],), 281 application_dicts=dict(type='list', elements='dict', 282 options=dict( 283 application=dict(required=True, type='str', 284 choices=['console', 'http', 'ontapi', 'rsh', 'snmp', 285 'sp', 'service-processor', 'service_processor', 'ssh', 'telnet'],), 286 authentication_methods=dict(required=True, type='list', elements='str', 287 choices=['community', 'password', 'publickey', 'domain', 'nsswitch', 'usm', 'cert']), 288 second_authentication_method=dict(type='str', choices=['none', 'password', 'publickey', 'nsswitch']))), 289 authentication_method=dict(type='str', 290 choices=['community', 'password', 'publickey', 'domain', 'nsswitch', 'usm', 'cert']), 291 set_password=dict(type='str', no_log=True), 292 role_name=dict(type='str'), 293 lock_user=dict(type='bool'), 294 vserver=dict(required=True, type='str', aliases=['svm']), 295 authentication_protocol=dict(type='str', choices=['none', 'md5', 'sha', 'sha2-256']), 296 authentication_password=dict(type='str', no_log=True), 297 engine_id=dict(type='str'), 298 privacy_protocol=dict(type='str', choices=['none', 'des', 'aes128']), 299 privacy_password=dict(type='str', no_log=True), 300 remote_switch_ipaddress=dict(type='str'), 301 replace_existing_apps_and_methods=dict(type='str', choices=['always', 'auto'], default='auto') 302 )) 303 304 self.module = AnsibleModule( 305 argument_spec=self.argument_spec, 306 required_if=[ 307 ('state', 'present', ['role_name']), 308 ('state', 'present', ['application_strs', 'application_dicts'], True) 309 ], 310 mutually_exclusive=[ 311 ('application_strs', 'application_dicts') 312 ], 313 required_together=[ 314 ('application_strs', 'authentication_method') 315 ], 316 supports_check_mode=True 317 ) 318 319 self.na_helper = NetAppModule() 320 self.parameters = self.na_helper.set_parameters(self.module.params) 321 self.strs_to_dicts() 322 323 # REST API should be used for ONTAP 9.6 or higher 324 self.rest_api = OntapRestAPI(self.module) 325 # some attributes are not supported in earlier REST implementation 326 unsupported_rest_properties = ['authentication_password', 'authentication_protocol', 'engine_id', 327 'privacy_password', 'privacy_protocol'] 328 used_unsupported_rest_properties = [x for x in unsupported_rest_properties if x in self.parameters] 329 self.use_rest, error = self.rest_api.is_rest(used_unsupported_rest_properties) 330 if error is not None: 331 self.module.fail_json(msg=error) 332 if not self.use_rest: 333 if not HAS_NETAPP_LIB: 334 self.module.fail_json(msg="the python NetApp-Lib module is required") 335 else: 336 if self.parameters['applications'] is None: 337 self.module.fail_json(msg="application_dicts or application_strs is a required parameter with ZAPI") 338 self.server = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=self.parameters['vserver']) 339 elif self.parameters['applications']: 340 if any(application['application'] == 'snmp' for application in self.parameters['applications']): 341 self.module.fail_json(msg="snmp as application is not supported in REST.") 342 for application in self.parameters['applications']: 343 # REST prefers certificate to cert 344 application['authentication_methods'] = ['certificate' if x == 'cert' else x for x in application['authentication_methods']] 345 # REST get always returns 'second_authentication_method' 346 if 'second_authentication_method' not in application: 347 application['second_authentication_method'] = None 348 349 def strs_to_dicts(self): 350 """transform applications list of strs to a list of dicts if application_strs in use""" 351 if 'application_dicts' in self.parameters: 352 self.parameters['applications'] = self.parameters['application_dicts'] 353 self.parameters['replace_existing_apps_and_methods'] = 'always' 354 elif 'application_strs' in self.parameters: 355 # actual conversion 356 self.parameters['applications'] = [ 357 dict(application=application, 358 authentication_methods=[self.parameters['authentication_method']], 359 second_authentication_method=None 360 ) for application in self.parameters['application_strs']] 361 else: 362 self.parameters['applications'] = None 363 364 def get_user_rest(self): 365 api = 'security/accounts' 366 params = { 367 'name': self.parameters['name'] 368 } 369 if self.parameters.get('vserver') is None: 370 # vserser is empty for cluster 371 params['scope'] = 'cluster' 372 else: 373 params['owner.name'] = self.parameters['vserver'] 374 375 message, error = self.rest_api.get(api, params) 376 if error: 377 self.module.fail_json(msg='Error while fetching user info: %s' % error) 378 if message['num_records'] == 1: 379 return message['records'][0]['owner']['uuid'], message['records'][0]['name'] 380 if message['num_records'] > 1: 381 self.module.fail_json(msg='Error while fetching user info, found multiple entries: %s' % repr(message)) 382 383 return None 384 385 def get_user_details_rest(self, name, uuid): 386 params = { 387 'fields': 'role,applications,locked' 388 } 389 api = "security/accounts/%s/%s" % (uuid, name) 390 message, error = self.rest_api.get(api, params) 391 if error: 392 self.module.fail_json(msg='Error while fetching user details: %s' % error) 393 if message: 394 # replace "none" values with None for comparison 395 for application in message['applications']: 396 if application.get('second_authentication_method') == 'none': 397 application['second_authentication_method'] = None 398 return_value = { 399 'role_name': message['role']['name'], 400 'applications': message['applications'] 401 } 402 if "locked" in message: 403 return_value['lock_user'] = message['locked'] 404 return return_value 405 406 def get_user(self): 407 """ 408 Checks if the user exists. 409 :param: application: application to grant access to, a dict 410 :return: 411 Dictionary if user found 412 None if user is not found 413 """ 414 desired_applications = [application['application'] for application in self.parameters['applications']] 415 desired_method = self.parameters.get('authentication_method') 416 security_login_get_iter = netapp_utils.zapi.NaElement('security-login-get-iter') 417 query_details = netapp_utils.zapi.NaElement.create_node_with_children( 418 'security-login-account-info', **{'vserver': self.parameters['vserver'], 419 'user-name': self.parameters['name']}) 420 421 query = netapp_utils.zapi.NaElement('query') 422 query.add_child_elem(query_details) 423 security_login_get_iter.add_child_elem(query) 424 try: 425 result = self.server.invoke_successfully(security_login_get_iter, 426 enable_tunneling=False) 427 if result.get_child_by_name('num-records') and \ 428 int(result.get_child_content('num-records')) >= 1: 429 applications = dict() 430 attr = result.get_child_by_name('attributes-list') 431 for info in attr.get_children(): 432 lock_user = self.na_helper.get_value_for_bool(True, info.get_child_content('is-locked')) 433 role_name = info.get_child_content('role-name') 434 application = info.get_child_content('application') 435 auth_method = info.get_child_content('authentication-method') 436 sec_method = info.get_child_content('second-authentication-method') 437 if self.parameters['replace_existing_apps_and_methods'] == 'always' and application in applications: 438 # with auto, only a single method is supported 439 applications[application][0].append(auth_method) 440 if sec_method != 'none': 441 applications[application][1] = sec_method 442 else: 443 if self.parameters['replace_existing_apps_and_methods'] == 'always' or\ 444 (application in desired_applications and auth_method == desired_method): 445 # with 'auto' we ignore existing apps that were not asked for 446 applications[application] = ([auth_method], None) 447 448 apps = [dict(application=application, authentication_methods=methods, second_authentication_method=sec_method) 449 for application, (methods, sec_method) in applications.items()] 450 return dict( 451 lock_user=lock_user, 452 role_name=role_name, 453 applications=apps 454 ) 455 return None 456 except netapp_utils.zapi.NaApiError as error: 457 # Error 16034 denotes a user not being found. 458 if to_native(error.code) == "16034": 459 return None 460 # Error 16043 denotes the user existing, but the application missing 461 elif to_native(error.code) == "16043": 462 return None 463 else: 464 self.module.fail_json(msg='Error getting user %s: %s' % (self.parameters['name'], to_native(error)), 465 exception=traceback.format_exc()) 466 467 def create_user_rest(self, apps=None): 468 if apps is not None: 469 api = 'security/accounts' 470 params = { 471 'name': self.parameters['name'], 472 'role.name': self.parameters['role_name'], 473 'applications': self.na_helper.filter_out_none_entries(apps) 474 } 475 if self.parameters.get('vserver') is not None: 476 # vserser is empty for cluster 477 params['owner.name'] = self.parameters['vserver'] 478 if 'set_password' in self.parameters: 479 params['password'] = self.parameters['set_password'] 480 if 'lock_user' in self.parameters: 481 params['locked'] = self.parameters['lock_user'] 482 dummy, error = self.rest_api.post(api, params) 483 error_sp = None 484 if error: 485 if 'invalid value' in error['message']: 486 if 'service-processor' in error['message'] or 'service_processor' in error['message']: 487 # find if there is error for service processor application value 488 # update value as per ONTAP version support 489 app_list_sp = params['applications'] 490 for app_item in app_list_sp: 491 if app_item['application'] == 'service-processor': 492 app_item['application'] = 'service_processor' 493 elif app_item['application'] == 'service_processor': 494 app_item['application'] = 'service-processor' 495 params['applications'] = app_list_sp 496 # post again and throw first error in case of an error 497 dummy, error_sp = self.rest_api.post(api, params) 498 if error_sp: 499 self.module.fail_json(msg='Error while creating user: %s' % error) 500 return True 501 502 # non-sp errors thrown 503 if error: 504 self.module.fail_json(msg='Error while creating user: %s' % error) 505 506 def create_user(self, application): 507 for index in range(len(application['authentication_methods'])): 508 self.create_user_with_auth(application, index) 509 510 def create_user_with_auth(self, application, index): 511 """ 512 creates the user for the given application and authentication_method 513 application is now a directory 514 :param: application: application to grant access to 515 """ 516 user_create = netapp_utils.zapi.NaElement.create_node_with_children( 517 'security-login-create', **{'vserver': self.parameters['vserver'], 518 'user-name': self.parameters['name'], 519 'application': application['application'], 520 'authentication-method': application['authentication_methods'][index], 521 'role-name': self.parameters.get('role_name')}) 522 if application.get('second_authentication_method') is not None: 523 user_create.add_new_child('second-authentication-method', application['second_authentication_method']) 524 if self.parameters.get('set_password') is not None: 525 user_create.add_new_child('password', self.parameters.get('set_password')) 526 if application['authentication_methods'][0] == 'usm': 527 if self.parameters.get('remote_switch_ipaddress') is not None: 528 user_create.add_new_child('remote-switch-ipaddress', self.parameters.get('remote_switch_ipaddress')) 529 snmpv3_login_info = netapp_utils.zapi.NaElement('snmpv3-login-info') 530 if self.parameters.get('authentication_password') is not None: 531 snmpv3_login_info.add_new_child('authentication-password', self.parameters['authentication_password']) 532 if self.parameters.get('authentication_protocol') is not None: 533 snmpv3_login_info.add_new_child('authentication-protocol', self.parameters['authentication_protocol']) 534 if self.parameters.get('engine_id') is not None: 535 snmpv3_login_info.add_new_child('engine-id', self.parameters['engine_id']) 536 if self.parameters.get('privacy_password') is not None: 537 snmpv3_login_info.add_new_child('privacy-password', self.parameters['privacy_password']) 538 if self.parameters.get('privacy_protocol') is not None: 539 snmpv3_login_info.add_new_child('privacy-protocol', self.parameters['privacy_protocol']) 540 user_create.add_child_elem(snmpv3_login_info) 541 542 try: 543 self.server.invoke_successfully(user_create, 544 enable_tunneling=False) 545 except netapp_utils.zapi.NaApiError as error: 546 self.module.fail_json(msg='Error creating user %s: %s' % (self.parameters['name'], to_native(error)), 547 exception=traceback.format_exc()) 548 549 def lock_unlock_user_rest(self, useruuid, username, value=None): 550 data = { 551 'locked': value 552 } 553 params = { 554 'name': self.parameters['name'], 555 'owner.uuid': useruuid, 556 } 557 api = "security/accounts/%s/%s" % (useruuid, username) 558 dummy, error = self.rest_api.patch(api, data, params) 559 if error: 560 self.module.fail_json(msg='Error while locking/unlocking user: %s' % error) 561 562 def lock_given_user(self): 563 """ 564 locks the user 565 566 :return: 567 True if user locked 568 False if lock user is not performed 569 :rtype: bool 570 """ 571 user_lock = netapp_utils.zapi.NaElement.create_node_with_children( 572 'security-login-lock', **{'vserver': self.parameters['vserver'], 573 'user-name': self.parameters['name']}) 574 575 try: 576 self.server.invoke_successfully(user_lock, 577 enable_tunneling=False) 578 except netapp_utils.zapi.NaApiError as error: 579 self.module.fail_json(msg='Error locking user %s: %s' % (self.parameters['name'], to_native(error)), 580 exception=traceback.format_exc()) 581 582 def unlock_given_user(self): 583 """ 584 unlocks the user 585 586 :return: 587 True if user unlocked 588 False if unlock user is not performed 589 :rtype: bool 590 """ 591 user_unlock = netapp_utils.zapi.NaElement.create_node_with_children( 592 'security-login-unlock', **{'vserver': self.parameters['vserver'], 593 'user-name': self.parameters['name']}) 594 595 try: 596 self.server.invoke_successfully(user_unlock, 597 enable_tunneling=False) 598 except netapp_utils.zapi.NaApiError as error: 599 if to_native(error.code) == '13114': 600 return False 601 else: 602 self.module.fail_json(msg='Error unlocking user %s: %s' % (self.parameters['name'], to_native(error)), 603 exception=traceback.format_exc()) 604 return True 605 606 def delete_user_rest(self): 607 uuid, username = self.get_user_rest() 608 api = "security/accounts/%s/%s" % (uuid, username) 609 dummy, error = self.rest_api.delete(api) 610 if error: 611 self.module.fail_json(msg='Error while deleting user : %s' % error) 612 613 def delete_user(self, application, methods_to_keep=None): 614 for index, method in enumerate(application['authentication_methods']): 615 if methods_to_keep is None or method not in methods_to_keep: 616 self.delete_user_with_auth(application, index) 617 618 def delete_user_with_auth(self, application, index): 619 """ 620 deletes the user for the given application and authentication_method 621 application is now a dict 622 :param: application: application to grant access to 623 """ 624 user_delete = netapp_utils.zapi.NaElement.create_node_with_children( 625 'security-login-delete', **{'vserver': self.parameters['vserver'], 626 'user-name': self.parameters['name'], 627 'application': application['application'], 628 'authentication-method': application['authentication_methods'][index]}) 629 630 try: 631 self.server.invoke_successfully(user_delete, 632 enable_tunneling=False) 633 except netapp_utils.zapi.NaApiError as error: 634 self.module.fail_json(msg='Error removing user %s: %s - application: %s' 635 % (self.parameters['name'], to_native(error), application), 636 exception=traceback.format_exc()) 637 638 @staticmethod 639 def is_repeated_password(message): 640 return message.startswith('New password must be different than last 6 passwords.') \ 641 or message.startswith('New password must be different from last 6 passwords.') \ 642 or message.startswith('New password must be different than the old password.') \ 643 or message.startswith('New password must be different from the old password.') 644 645 def change_password_rest(self, useruuid, username): 646 data = { 647 'password': self.parameters['set_password'], 648 } 649 params = { 650 'name': self.parameters['name'], 651 'owner.uuid': useruuid, 652 } 653 api = "security/accounts/%s/%s" % (useruuid, username) 654 dummy, error = self.rest_api.patch(api, data, params) 655 if error: 656 if 'message' in error and self.is_repeated_password(error['message']): 657 # if the password is reused, assume idempotency 658 return False 659 else: 660 self.module.fail_json(msg='Error while updating user password: %s' % error) 661 return True 662 663 def change_password(self): 664 """ 665 Changes the password 666 667 :return: 668 True if password updated 669 False if password is not updated 670 :rtype: bool 671 """ 672 # self.server.set_vserver(self.parameters['vserver']) 673 modify_password = netapp_utils.zapi.NaElement.create_node_with_children( 674 'security-login-modify-password', **{ 675 'new-password': str(self.parameters.get('set_password')), 676 'user-name': self.parameters['name']}) 677 try: 678 self.server.invoke_successfully(modify_password, 679 enable_tunneling=True) 680 except netapp_utils.zapi.NaApiError as error: 681 if to_native(error.code) == '13114': 682 return False 683 # if the user give the same password, instead of returning an error, return ok 684 if to_native(error.code) == '13214' and self.is_repeated_password(error.message): 685 return False 686 self.module.fail_json(msg='Error setting password for user %s: %s' % (self.parameters['name'], to_native(error)), 687 exception=traceback.format_exc()) 688 689 self.server.set_vserver(None) 690 return True 691 692 def modify_apps_rest(self, useruuid, username, apps=None): 693 data = { 694 'role.name': self.parameters['role_name'], 695 'applications': self.na_helper.filter_out_none_entries(apps) 696 } 697 params = { 698 'name': self.parameters['name'], 699 'owner.uuid': useruuid, 700 } 701 api = "security/accounts/%s/%s" % (useruuid, username) 702 dummy, error = self.rest_api.patch(api, data, params) 703 if error: 704 self.module.fail_json(msg='Error while modifying user details: %s' % error) 705 706 def modify_user(self, application, current_methods): 707 for index, method in enumerate(application['authentication_methods']): 708 if method in current_methods: 709 self.modify_user_with_auth(application, index) 710 else: 711 self.create_user_with_auth(application, index) 712 713 def modify_user_with_auth(self, application, index): 714 """ 715 Modify user 716 application is now a dict 717 """ 718 user_modify = netapp_utils.zapi.NaElement.create_node_with_children( 719 'security-login-modify', **{'vserver': self.parameters['vserver'], 720 'user-name': self.parameters['name'], 721 'application': application['application'], 722 'authentication-method': application['authentication_methods'][index], 723 'role-name': self.parameters.get('role_name')}) 724 725 try: 726 self.server.invoke_successfully(user_modify, 727 enable_tunneling=False) 728 except netapp_utils.zapi.NaApiError as error: 729 self.module.fail_json(msg='Error modifying user %s: %s' % (self.parameters['name'], to_native(error)), 730 exception=traceback.format_exc()) 731 732 def change_sp_application(self, current_apps): 733 """Adjust requested app name to match ONTAP convention""" 734 if not self.parameters['applications']: 735 return 736 app_list = [app['application'] for app in current_apps] 737 for application in self.parameters['applications']: 738 if application['application'] == 'service_processor' and 'service-processor' in app_list: 739 application['application'] = 'service-processor' 740 elif application['application'] == 'service-processor' and 'service_processor' in app_list: 741 application['application'] = 'service_processor' 742 743 def apply(self): 744 if self.use_rest: 745 current = self.get_user_rest() 746 if current is not None: 747 uuid, name = current 748 current = self.get_user_details_rest(name, uuid) 749 self.change_sp_application(current['applications']) 750 else: 751 netapp_utils.ems_log_event("na_ontap_user", self.server) 752 current = self.get_user() 753 754 cd_action = self.na_helper.get_cd_action(current, self.parameters) 755 modify_decision = self.na_helper.get_modified_attributes(current, self.parameters) 756 757 if self.use_rest and current and 'lock_user' not in current: 758 # REST does not return locked if password is not set 759 if cd_action is None and self.parameters.get('lock_user') is not None: 760 if self.parameters.get('set_password') is None: 761 self.module.fail_json(msg='Error: cannot modify lock state if password is not set.') 762 modify_decision['lock_user'] = self.parameters['lock_user'] 763 self.na_helper.changed = True 764 765 if self.na_helper.changed and not self.module.check_mode: 766 if cd_action == 'create': 767 if self.use_rest: 768 self.create_user_rest(self.parameters['applications']) 769 else: 770 for application in self.parameters['applications']: 771 self.create_user(application) 772 elif cd_action == 'delete': 773 if self.use_rest: 774 self.delete_user_rest() 775 else: 776 for application in current['applications']: 777 self.delete_user(application) 778 elif modify_decision: 779 if self.use_rest: 780 if 'role_name' in modify_decision or 'applications' in modify_decision: 781 self.modify_apps_rest(uuid, name, self.parameters['applications']) 782 else: 783 if 'role_name' in modify_decision or 'applications' in modify_decision: 784 if 'applications' not in modify_decision: 785 # to change roles, we need at least one app 786 modify_decision['applications'] = self.parameters['applications'] 787 current_apps = dict((application['application'], application['authentication_methods']) for application in current['applications']) 788 for application in modify_decision['applications']: 789 if application['application'] in current_apps: 790 self.modify_user(application, current_apps[application['application']]) 791 else: 792 self.create_user(application) 793 desired_apps = dict((application['application'], application['authentication_methods']) 794 for application in self.parameters['applications']) 795 for application in current['applications']: 796 if application['application'] not in desired_apps: 797 self.delete_user(application) 798 else: 799 self.delete_user(application, desired_apps[application['application']]) 800 801 if cd_action is None and self.parameters.get('set_password') is not None: 802 # if check_mode, don't attempt to change the password, but assume it would be changed 803 if self.use_rest: 804 self.na_helper.changed = self.module.check_mode or self.change_password_rest(uuid, name) 805 else: 806 self.na_helper.changed = self.module.check_mode or self.change_password() 807 808 if cd_action is None and self.na_helper.changed and not self.module.check_mode: 809 # lock/unlock actions require password to be set 810 if modify_decision and 'lock_user' in modify_decision: 811 if self.use_rest: 812 self.lock_unlock_user_rest(uuid, name, self.parameters['lock_user']) 813 else: 814 if self.parameters.get('lock_user'): 815 self.lock_given_user() 816 else: 817 self.unlock_given_user() 818 819 self.module.exit_json(changed=self.na_helper.changed, current=current, modify=modify_decision) 820 821 822def main(): 823 obj = NetAppOntapUser() 824 obj.apply() 825 826 827if __name__ == '__main__': 828 main() 829