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