1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# 5# Dell EMC OpenManage Ansible Modules 6# Version 3.5.0 7# Copyright (C) 2018-2021 Dell Inc. or its subsidiaries. All Rights Reserved. 8 9# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 10# 11 12 13from __future__ import (absolute_import, division, print_function) 14__metaclass__ = type 15 16DOCUMENTATION = """ 17--- 18module: idrac_user 19short_description: Configure settings for user accounts 20version_added: "2.1.0" 21description: 22 - This module allows to perform the following, 23 - Add a new user account. 24 - Edit a user account. 25 - Enable or Disable a user account. 26extends_documentation_fragment: 27 - dellemc.openmanage.idrac_auth_options 28options: 29 state: 30 type: str 31 description: 32 - Select C(present) to create or modify a user account. 33 - Select C(absent) to remove a user account. 34 - Ensure Lifecycle Controller is available because the user operation 35 uses the capabilities of Lifecycle Controller. 36 choices: [present, absent] 37 default: present 38 user_name: 39 type: str 40 required: True 41 description: Provide the I(user_name) of the account to be created, deleted or modified. 42 user_password: 43 type: str 44 description: 45 - Provide the password for the user account. The password can be changed when the user account is modified. 46 - To ensure security, the I(user_password) must be at least eight characters long and must contain 47 lowercase and upper-case characters, numbers, and special characters. 48 new_user_name: 49 type: str 50 description: Provide the I(user_name) for the account to be modified. 51 privilege: 52 type: str 53 description: 54 - Following are the role-based privileges. 55 - A user with C(Administrator) privilege can log in to iDRAC, and then configure iDRAC, configure users, 56 clear logs, control and configure system, access virtual console, access virtual media, test alerts, 57 and execute debug commands. 58 - A user with C(Operator) privilege can log in to iDRAC, and then configure iDRAC, control and configure system, 59 access virtual console, access virtual media, and execute debug commands. 60 - A user with C(ReadOnly) privilege can only log in to iDRAC. 61 - A user with C(None), no privileges assigned. 62 choices: [Administrator, ReadOnly, Operator, None] 63 ipmi_lan_privilege: 64 type: str 65 description: The Intelligent Platform Management Interface LAN privilege level assigned to the user. 66 choices: [Administrator, Operator, User, No Access] 67 ipmi_serial_privilege: 68 type: str 69 description: 70 - The Intelligent Platform Management Interface Serial Port privilege level assigned to the user. 71 - This option is only applicable for rack and tower servers. 72 choices: [Administrator, Operator, User, No Access] 73 enable: 74 type: bool 75 description: Provide the option to enable or disable a user from logging in to iDRAC. 76 sol_enable: 77 type: bool 78 description: Enables Serial Over Lan (SOL) for an iDRAC user. 79 protocol_enable: 80 type: bool 81 description: Enables protocol for the iDRAC user. 82 authentication_protocol: 83 type: str 84 description: 85 - This option allows to configure one of the following authentication protocol 86 types to authenticate the iDRAC user. 87 - Secure Hash Algorithm C(SHA). 88 - Message Digest 5 C(MD5). 89 - An authentication protocol is not configured if C(None) is selected. 90 choices: [None, SHA, MD5] 91 privacy_protocol: 92 type: str 93 description: 94 - This option allows to configure one of the following privacy encryption protocols for the iDRAC user. 95 - Data Encryption Standard C(DES). 96 - Advanced Encryption Standard C(AES). 97 - A privacy protocol is not configured if C(None) is selected. 98 choices: [None, DES, AES] 99requirements: 100 - "python >= 2.7.5" 101author: "Felix Stephen (@felixs88)" 102notes: 103 - Run this module from a system that has direct access to DellEMC iDRAC. 104 - This module supports C(check_mode). 105""" 106 107EXAMPLES = """ 108--- 109- name: Configure a new iDRAC user 110 dellemc.openmanage.idrac_user: 111 idrac_ip: 198.162.0.1 112 idrac_user: idrac_user 113 idrac_password: idrac_password 114 state: present 115 user_name: user_name 116 user_password: user_password 117 privilege: Administrator 118 ipmi_lan_privilege: Administrator 119 ipmi_serial_privilege: Administrator 120 enable: true 121 sol_enable: true 122 protocol_enable: true 123 authentication_protocol: SHA 124 privacy_protocol: AES 125 126- name: Modify existing iDRAC user username and password 127 dellemc.openmanage.idrac_user: 128 idrac_ip: 198.162.0.1 129 idrac_user: idrac_user 130 idrac_password: idrac_password 131 state: present 132 user_name: user_name 133 new_user_name: new_user_name 134 user_password: user_password 135 136- name: Delete existing iDRAC user account 137 dellemc.openmanage.idrac_user: 138 idrac_ip: 198.162.0.1 139 idrac_user: idrac_user 140 idrac_password: idrac_password 141 state: absent 142 user_name: user_name 143""" 144 145RETURN = r''' 146--- 147msg: 148 description: Status of the iDRAC user configuration. 149 returned: always 150 type: str 151 sample: "Successfully created user account details." 152status: 153 description: Configures the iDRAC users attributes. 154 returned: success 155 type: dict 156 sample: { 157 "@Message.ExtendedInfo": [{ 158 "Message": "Successfully Completed Request", 159 "MessageArgs": [], 160 "MessageArgs@odata.count": 0, 161 "MessageId": "Base.1.5.Success", 162 "RelatedProperties": [], 163 "RelatedProperties@odata.count": 0, 164 "Resolution": "None", 165 "Severity": "OK" 166 }, { 167 "Message": "The operation successfully completed.", 168 "MessageArgs": [], 169 "MessageArgs@odata.count": 0, 170 "MessageId": "IDRAC.2.1.SYS413", 171 "RelatedProperties": [], 172 "RelatedProperties@odata.count": 0, 173 "Resolution": "No response action is required.", 174 "Severity": "Informational"} 175 ]} 176error_info: 177 description: Details of the HTTP Error. 178 returned: on HTTP error 179 type: dict 180 sample: { 181 "error": { 182 "code": "Base.1.0.GeneralError", 183 "message": "A general error has occurred. See ExtendedInfo for more information.", 184 "@Message.ExtendedInfo": [ 185 { 186 "MessageId": "GEN1234", 187 "RelatedProperties": [], 188 "Message": "Unable to process the request because an error occurred.", 189 "MessageArgs": [], 190 "Severity": "Critical", 191 "Resolution": "Retry the operation. If the issue persists, contact your system administrator." 192 } 193 ] 194 } 195 } 196''' 197 198 199import json 200import re 201import time 202from ansible.module_utils.six.moves.urllib.error import URLError, HTTPError 203from ansible.module_utils.urls import ConnectionError, SSLValidationError 204from ansible_collections.dellemc.openmanage.plugins.module_utils.idrac_redfish import iDRACRedfishAPI 205from ansible.module_utils.basic import AnsibleModule 206 207 208ACCOUNT_URI = "/redfish/v1/Managers/iDRAC.Embedded.1/Accounts/" 209ATTRIBUTE_URI = "/redfish/v1/Managers/iDRAC.Embedded.1/Attributes/" 210PRIVILEGE = {"Administrator": 511, "Operator": 499, "ReadOnly": 1, "None": 0} 211ACCESS = {0: "Disabled", 1: "Enabled"} 212 213 214def compare_payload(json_payload, idrac_attr): 215 """ 216 :param json_payload: json payload created for update operation 217 :param idrac_attr: idrac user attributes 218 case1: always skip password for difference 219 case2: as idrac_attr returns privilege in the format of string so 220 convert payload to string only for comparision 221 :return: bool 222 """ 223 copy_json = json_payload.copy() 224 for key, val in dict(copy_json).items(): 225 split_key = key.split("#")[1] 226 if split_key == "Password": 227 is_change_required = True 228 break 229 if split_key == "Privilege": 230 copy_json[key] = str(val) 231 else: 232 is_change_required = bool(list(set(copy_json.items()) - set(idrac_attr.items()))) 233 return is_change_required 234 235 236def get_user_account(module, idrac): 237 """ 238 This function gets the slot id and slot uri for create and modify. 239 :param module: ansible module arguments 240 :param idrac: idrac objects 241 :return: user_attr, slot_uri, slot_id, empty_slot, empty_slot_uri 242 """ 243 slot_uri, slot_id, empty_slot, empty_slot_uri = None, None, None, None 244 if not module.params["user_name"]: 245 module.fail_json(msg="User name is not valid.") 246 response = idrac.export_scp(export_format="JSON", export_use="Default", target="IDRAC", job_wait=True) 247 user_attributes = idrac.get_idrac_local_account_attr(response.json_data, fqdd="iDRAC.Embedded.1") 248 slot_num = tuple(range(2, 17)) 249 for num in slot_num: 250 user_name = "Users.{0}#UserName".format(num) 251 if user_attributes.get(user_name) == module.params["user_name"]: 252 slot_id = num 253 slot_uri = ACCOUNT_URI + str(num) 254 break 255 if not user_attributes.get(user_name) and (empty_slot_uri and empty_slot) is None: 256 empty_slot = num 257 empty_slot_uri = ACCOUNT_URI + str(num) 258 return user_attributes, slot_uri, slot_id, empty_slot, empty_slot_uri 259 260 261def get_payload(module, slot_id, action=None): 262 """ 263 This function creates the payload with slot id. 264 :param module: ansible module arguments 265 :param action: new user name is only applicable in case of update user name. 266 :param slot_id: slot id for user slot 267 :return: json data with slot id 268 """ 269 slot_payload = {"Users.{0}.UserName": module.params["user_name"], 270 "Users.{0}.Password": module.params["user_password"], 271 "Users.{0}.Enable": ACCESS.get(module.params["enable"]), 272 "Users.{0}.Privilege": PRIVILEGE.get(module.params["privilege"]), 273 "Users.{0}.IpmiLanPrivilege": module.params["ipmi_lan_privilege"], 274 "Users.{0}.IpmiSerialPrivilege": module.params["ipmi_serial_privilege"], 275 "Users.{0}.SolEnable": ACCESS.get(module.params["sol_enable"]), 276 "Users.{0}.ProtocolEnable": ACCESS.get(module.params["protocol_enable"]), 277 "Users.{0}.AuthenticationProtocol": module.params["authentication_protocol"], 278 "Users.{0}.PrivacyProtocol": module.params["privacy_protocol"], } 279 if module.params["new_user_name"] is not None and action == "update": 280 user_name = "Users.{0}.UserName".format(slot_id) 281 slot_payload[user_name] = module.params["new_user_name"] 282 elif module.params["state"] == "absent": 283 slot_payload = {"Users.{0}.UserName": "", "Users.{0}.Enable": "Disabled", "Users.{0}.Privilege": 0, 284 "Users.{0}.IpmiLanPrivilege": "No Access", "Users.{0}.IpmiSerialPrivilege": "No Access", 285 "Users.{0}.SolEnable": "Disabled", "Users.{0}.ProtocolEnable": "Disabled", 286 "Users.{0}.AuthenticationProtocol": "SHA", "Users.{0}.PrivacyProtocol": "AES"} 287 payload = dict([(k.format(slot_id), v) for k, v in slot_payload.items() if v is not None]) 288 return payload 289 290 291def convert_payload_xml(payload): 292 """ 293 this function converts payload to xml and json data. 294 :param payload: user input for payload 295 :return: returns xml and json data 296 """ 297 root = """<SystemConfiguration><Component FQDD="iDRAC.Embedded.1">{0}</Component></SystemConfiguration>""" 298 attr = "" 299 json_payload = {} 300 for k, v in payload.items(): 301 key = re.sub(r"(?<=\d)\.", "#", k) 302 attr += '<Attribute Name="{0}">{1}</Attribute>'.format(key, v) 303 json_payload[key] = v 304 root = root.format(attr) 305 return root, json_payload 306 307 308def create_or_modify_account(module, idrac, slot_uri, slot_id, empty_slot_id, empty_slot_uri, user_attr): 309 """ 310 This function create user account in case not exists else update it. 311 :param module: user account module arguments 312 :param idrac: idrac object 313 :param slot_uri: slot uri for update 314 :param slot_id: slot id for update 315 :param empty_slot_id: empty slot id for create 316 :param empty_slot_uri: empty slot uri for create 317 :return: json 318 """ 319 generation, firmware_version = idrac.get_server_generation 320 msg, response = "Unable to retrieve the user details.", {} 321 if (slot_id and slot_uri) is None and (empty_slot_id and empty_slot_uri) is not None: 322 msg = "Successfully created user account." 323 payload = get_payload(module, empty_slot_id, action="create") 324 if module.check_mode: 325 module.exit_json(msg="Changes found to commit!", changed=True) 326 if generation >= 14: 327 response = idrac.invoke_request(ATTRIBUTE_URI, "PATCH", data={"Attributes": payload}) 328 elif generation < 14: 329 xml_payload, json_payload = convert_payload_xml(payload) 330 time.sleep(10) 331 response = idrac.import_scp(import_buffer=xml_payload, target="ALL", job_wait=True) 332 elif (slot_id and slot_uri) is not None: 333 msg = "Successfully updated user account." 334 payload = get_payload(module, slot_id, action="update") 335 xml_payload, json_payload = convert_payload_xml(payload) 336 value = compare_payload(json_payload, user_attr) 337 if module.check_mode: 338 if value: 339 module.exit_json(msg="Changes found to commit!", changed=True) 340 module.exit_json(msg="No changes found to commit!") 341 if not value: 342 module.exit_json(msg="Requested changes are already present in the user slot.") 343 if generation >= 14: 344 response = idrac.invoke_request(ATTRIBUTE_URI, "PATCH", data={"Attributes": payload}) 345 elif generation < 14: 346 time.sleep(10) 347 response = idrac.import_scp(import_buffer=xml_payload, target="ALL", job_wait=True) 348 elif (slot_id and slot_uri and empty_slot_id and empty_slot_uri) is None: 349 module.fail_json(msg="Maximum number of users reached. Delete a user account and retry the operation.") 350 return response, msg 351 352 353def remove_user_account(module, idrac, slot_uri, slot_id): 354 """ 355 remove user user account by passing empty payload details. 356 :param module: user account module arguments. 357 :param idrac: idrac object. 358 :param slot_uri: user slot uri. 359 :param slot_id: user slot id. 360 :return: json. 361 """ 362 response, msg = {}, "Successfully deleted user account." 363 payload = get_payload(module, slot_id, action="delete") 364 xml_payload, json_payload = convert_payload_xml(payload) 365 if module.check_mode and (slot_id and slot_uri) is not None: 366 module.exit_json(msg="Changes found to commit!", changed=True) 367 elif module.check_mode and (slot_uri and slot_id) is None: 368 module.exit_json(msg="No changes found to commit!") 369 elif not module.check_mode and (slot_uri and slot_id) is not None: 370 time.sleep(10) 371 response = idrac.import_scp(import_buffer=xml_payload, target="ALL", job_wait=True) 372 else: 373 module.exit_json(msg="The user account is absent.") 374 return response, msg 375 376 377def main(): 378 module = AnsibleModule( 379 argument_spec={ 380 "idrac_ip": {"required": True}, 381 "idrac_user": {"required": True}, 382 "idrac_password": {"required": True, "aliases": ['idrac_pwd'], "no_log": True}, 383 "idrac_port": {"required": False, "default": 443, "type": 'int'}, 384 "state": {"required": False, "choices": ['present', 'absent'], "default": "present"}, 385 "new_user_name": {"required": False}, 386 "user_name": {"required": True}, 387 "user_password": {"required": False, "no_log": True}, 388 "privilege": {"required": False, "choices": ['Administrator', 'ReadOnly', 'Operator', 'None']}, 389 "ipmi_lan_privilege": {"required": False, "choices": ['Administrator', 'Operator', 'User', 'No Access']}, 390 "ipmi_serial_privilege": {"required": False, "choices": ['Administrator', 'Operator', 'User', 'No Access']}, 391 "enable": {"required": False, "type": "bool"}, 392 "sol_enable": {"required": False, "type": "bool"}, 393 "protocol_enable": {"required": False, "type": "bool"}, 394 "authentication_protocol": {"required": False, "choices": ['SHA', 'MD5', 'None']}, 395 "privacy_protocol": {"required": False, "choices": ['AES', 'DES', 'None']}, 396 }, 397 supports_check_mode=True) 398 try: 399 with iDRACRedfishAPI(module.params, req_session=True) as idrac: 400 user_attr, slot_uri, slot_id, empty_slot_id, empty_slot_uri = get_user_account(module, idrac) 401 if module.params["state"] == "present": 402 response, message = create_or_modify_account(module, idrac, slot_uri, slot_id, empty_slot_id, 403 empty_slot_uri, user_attr) 404 elif module.params["state"] == "absent": 405 response, message = remove_user_account(module, idrac, slot_uri, slot_id) 406 error = response.json_data.get("error") 407 oem = response.json_data.get("Oem") 408 if oem: 409 oem_msg = oem.get("Dell").get("Message") 410 error_msg = ["Unable to complete application of configuration profile values.", 411 "Import of Server Configuration Profile operation completed with errors."] 412 if oem_msg in error_msg: 413 module.fail_json(msg=oem_msg, error_info=response.json_data) 414 if error: 415 module.fail_json(msg=error.get("message"), error_info=response.json_data) 416 module.exit_json(msg=message, status=response.json_data, changed=True) 417 except HTTPError as err: 418 module.fail_json(msg=str(err), error_info=json.load(err)) 419 except URLError as err: 420 module.exit_json(msg=str(err), unreachable=True) 421 except (RuntimeError, SSLValidationError, ConnectionError, KeyError, 422 ImportError, ValueError, TypeError) as e: 423 module.fail_json(msg=str(e)) 424 425 426if __name__ == '__main__': 427 main() 428