diff --git a/plugins/module_utils/constants.py b/plugins/module_utils/constants.py index 5d09e74..02dbba3 100644 --- a/plugins/module_utils/constants.py +++ b/plugins/module_utils/constants.py @@ -168,3 +168,12 @@ ND_REST_KEYS_TO_SANITIZE = ["metadata"] ND_SETUP_NODE_DEPLOYMENT_TYPE = {"physical": "cimc", "virtual": "vnode"} + +USER_ROLES_MAPPING = { + "fabric_admin": "fabric-admin", + "observer": "observer", + "super_admin": "super-admin", + "support_engineer": "support-engineer", + "approver": "approver", + "designer": "designer", +} diff --git a/plugins/module_utils/nd.py b/plugins/module_utils/nd.py index cca3ed4..0a33be8 100644 --- a/plugins/module_utils/nd.py +++ b/plugins/module_utils/nd.py @@ -18,7 +18,6 @@ from ansible.module_utils.basic import json from ansible.module_utils.basic import env_fallback from ansible.module_utils.six import PY3 -from ansible.module_utils.six.moves import filterfalse from ansible.module_utils.six.moves.urllib.parse import urlencode from ansible.module_utils._text import to_native, to_text from ansible.module_utils.connection import Connection @@ -73,53 +72,27 @@ def cmp(a, b): def issubset(subset, superset): - """Recurse through nested dictionary and compare entries""" + """Recurse through a nested dictionary and check if it is a subset of another.""" - # Both objects are the same object - if subset is superset: - return True - - # Both objects are identical - if subset == superset: - return True - - # Both objects have a different type - if isinstance(subset) is not isinstance(superset): + if type(subset) is not type(superset): return False + if not isinstance(subset, dict): + if isinstance(subset, list): + return all(item in superset for item in subset) + return subset == superset + for key, value in subset.items(): - # Ignore empty values if value is None: - return True + continue - # Item from subset is missing from superset if key not in superset: return False - # Item has different types in subset and superset - if isinstance(superset.get(key)) is not isinstance(value): - return False + superset_value = superset.get(key) - # Compare if item values are subset - if isinstance(value, dict): - if not issubset(superset.get(key), value): - return False - elif isinstance(value, list): - try: - # NOTE: Fails for lists of dicts - if not set(value) <= set(superset.get(key)): - return False - except TypeError: - # Fall back to exact comparison for lists of dicts - diff = list(filterfalse(lambda i: i in value, superset.get(key))) + list(filterfalse(lambda j: j in superset.get(key), value)) - if diff: - return False - elif isinstance(value, set): - if not value <= superset.get(key): - return False - else: - if not value == superset.get(key): - return False + if not issubset(value, superset_value): + return False return True @@ -252,7 +225,7 @@ def request( if file is not None: info = conn.send_file_request(method, uri, file, data, None, file_key, file_ext) else: - if data: + if data is not None: info = conn.send_request(method, uri, json.dumps(data)) else: info = conn.send_request(method, uri) @@ -310,6 +283,8 @@ def request( self.fail_json(msg="ND Error: {0}".format(self.error.get("message")), data=data, info=info) self.error = payload if "code" in payload: + if self.status == 404 and ignore_not_found_error: + return {} self.fail_json(msg="ND Error {code}: {message}".format(**payload), data=data, info=info, payload=payload) elif "messages" in payload and len(payload.get("messages")) > 0: self.fail_json(msg="ND Error {code} ({severity}): {message}".format(**payload["messages"][0]), data=data, info=info, payload=payload) @@ -506,30 +481,27 @@ def get_diff(self, unwanted=None): if not self.existing and self.sent: return True - existing = self.existing - sent = self.sent + existing = deepcopy(self.existing) + sent = deepcopy(self.sent) for key in unwanted: if isinstance(key, str): if key in existing: - try: - del existing[key] - except KeyError: - pass - try: - del sent[key] - except KeyError: - pass + del existing[key] + if key in sent: + del sent[key] elif isinstance(key, list): key_path, last = key[:-1], key[-1] try: existing_parent = reduce(dict.get, key_path, existing) - del existing_parent[last] + if existing_parent is not None: + del existing_parent[last] except KeyError: pass try: sent_parent = reduce(dict.get, key_path, sent) - del sent_parent[last] + if sent_parent is not None: + del sent_parent[last] except KeyError: pass return not issubset(sent, existing) diff --git a/plugins/modules/nd_local_user.py b/plugins/modules/nd_local_user.py new file mode 100644 index 0000000..5598630 --- /dev/null +++ b/plugins/modules/nd_local_user.py @@ -0,0 +1,280 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2025, Gaspard Micol (@gmicol) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = {"metadata_version": "1.1", "status": ["preview"], "supported_by": "community"} + +DOCUMENTATION = r""" +--- +module: nd_local_user +version_added: "1.4.0" +short_description: Manage local users on Cisco Nexus Dashboard +description: +- Manage local users on Cisco Nexus Dashboard (ND). +- This module supports creating, updating, querying, and deleting local users. +- This module is only supported on ND v4.1.0 and later. +author: +- Gaspard Micol (@gmicol) +options: + email: + description: + - The email address of the local user. + type: str + login_id: + description: + - The login ID of the local user. + - The O(login_id) must be defined when creating, updating or deleting a local user. + type: str + first_name: + description: + - The first name of the local user. + type: str + last_name: + description: + - The last name of the local user. + type: str + user_password: + description: + - The password of the local user. + - Password must have a minimum of 8 characters to a maximum of 64 characters. + - Password must have three of the following; one number, one lower case character, one upper case character, one special character. + - The O(user_password) must be defined when creating a new local_user. + type: str + reuse_limitation: + description: + - The number of different passwords a user must use before they can reuse a previous one. + - It defaults to C(0) when unset during creation. + type: int + time_interval_limitation: + description: + - The minimum time period that must pass before a previous password can be reused. + - It defaults to C(0) when unset during creation. + type: int + security_domains: + description: + - The list of Security Domains and Roles for the local user. + - At least, one Security Domain must be defined when creating a new local user. + type: list + elements: dict + suboptions: + name: + description: + - The name of the Security Domain to which the local user is given access. + type: str + required: true + aliases: [ security_domain_name, domain_name ] + roles: + description: + - The Permission Roles of the local user within the Security Domain. + type: list + elements: str + choices: [ fabric_admin, observer, super_admin, support_engineer, approver, designer ] + aliases: [ domains ] + remote_id_claim: + description: + - The remote ID claim of the local user. + type: str + remote_user_authorization: + description: + - To enable/disable the Remote User Authorization of the local user. + - Remote User Authorization is used for signing into Nexus Dashboard when using identity providers that cannot provide authorization claims. + Once this attribute is enabled, the local user ID cannot be used to directly login to Nexus Dashboard. + - It defaults to C(false) when unset during creation. + type: bool + state: + description: + - Use C(present) to create or update a local user. + - Use C(absent) to delete an existing local user. + - Use C(query) for listing all the existing local users or a specific local user if O(login_id) is specified. + type: str + default: present + choices: [ present, absent, query ] +extends_documentation_fragment: +- cisco.nd.modules +- cisco.nd.check_mode +notes: +- This module is not idempotent when creating or updating a local user object when O(user_password) is used. +""" + +EXAMPLES = r""" +- name: Create a new local user + cisco.nd.nd_local_user: + email: user@example.com + login_id: local_user + first_name: User first name + last_name: User last name + user_password: localUserPassword1% + reuse_limitation: 20 + time_interval_limitation: 10 + security_domains: + name: all + roles: + - observer + - support_engineer + remote_id_claim: remote_user + remote_user_authorization: true + state: present + register: result + +- name: Create local user with minimal configuration + cisco.nd.nd_local_user: + login_id: local_user_min + user_password: localUserMinPassword1% + security_domain: all + state: present + +- name: Update local user + cisco.nd.nd_local_user: + email: udpateduser@example.com + login_id: local_user + first_name: Updated user first name + last_name: Updated user last name + user_password: updatedLocalUserPassword1% + reuse_limitation: 25 + time_interval_limitation: 15 + security_domains: + - name: all + roles: super_admin + - name: ansible_domain + roles: observer + roles: super_admin + remote_id_claim: "" + remote_user_authorization: false + state: present + +- name: Query an existing local user + cisco.nd.nd_local_user: + login_id: local_user + state: query + register: query_result + +- name: Query all local users + cisco.nd.nd_local_user: + state: query + register: query_all + +- name: Delete a local user + cisco.nd.nd_local_user: + login_id: local_user + state: absent +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.nd.plugins.module_utils.nd import NDModule, nd_argument_spec, sanitize_dict +from ansible_collections.cisco.nd.plugins.module_utils.constants import USER_ROLES_MAPPING + + +def main(): + argument_spec = nd_argument_spec() + argument_spec.update( + email=dict(type="str"), + login_id=dict(type="str"), + first_name=dict(type="str"), + last_name=dict(type="str"), + user_password=dict(type="str", no_log=True), + reuse_limitation=dict(type="int"), + time_interval_limitation=dict(type="int"), + security_domains=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str", required=True, aliases=["security_domain_name", "domain_name"]), + roles=dict(type="list", elements="str", choices=list(USER_ROLES_MAPPING)), + ), + aliases=["domains"], + ), + remote_id_claim=dict(type="str"), + remote_user_authorization=dict(type="bool"), + state=dict(type="str", default="present", choices=["present", "absent", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "present", ["login_id"]], + ["state", "absent", ["login_id"]], + ], + ) + + nd = NDModule(module) + + email = nd.params.get("email") + login_id = nd.params.get("login_id") + first_name = nd.params.get("first_name") + last_name = nd.params.get("last_name") + user_password = nd.params.get("user_password") + reuse_limitation = nd.params.get("reuse_limitation") + time_interval_limitation = nd.params.get("time_interval_limitation") + security_domains = nd.params.get("security_domains") + remote_id_claim = nd.params.get("remote_id_claim") + remote_user_authorization = nd.params.get("remote_user_authorization") + state = nd.params.get("state") + + path = "/api/v1/infra/aaa/localUsers" + if login_id: + login_id_path = "{0}/{1}".format(path, login_id) + nd.existing = nd.previous = nd.query_obj(path=login_id_path, ignore_not_found_error=True) + else: + nd.existing = nd.query_obj(path=path, ignore_not_found_error=True) + + if state == "present": + + payload = { + "email": email, + "firstName": first_name, + "lastName": last_name, + "loginID": login_id, + "password": user_password, + "remoteIDClaim": remote_id_claim, + "xLaunch": remote_user_authorization, + } + + if security_domains: + payload["rbac"] = { + "domains": { + security_domain.get("name"): { + "roles": [USER_ROLES_MAPPING.get(role) for role in security_domain["roles"]] if isinstance(security_domain.get("roles"), list) else [], + } + for security_domain in security_domains + }, + } + if reuse_limitation or time_interval_limitation: + payload["passwordPolicy"] = sanitize_dict( + { + "reuseLimitation": reuse_limitation, + "timeIntervalLimitation": time_interval_limitation, + } + ) + + nd.sanitize(payload) + + if module.check_mode: + nd.existing = nd.proposed + else: + if not nd.existing: + nd.existing = nd.request(path=path, method="POST", data=payload) + elif nd.get_diff(unwanted=[["passwordPolicy", "passwordChangeTime"], ["userID"]]): + nd.existing = nd.request(path=login_id_path, method="PUT", data=payload) + + elif state == "absent": + if nd.existing: + if not module.check_mode: + nd.request(path=login_id_path, method="DELETE") + nd.existing = {} + + nd.exit_json() + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/nd_local_user/tasks/main.yml b/tests/integration/targets/nd_local_user/tasks/main.yml new file mode 100644 index 0000000..1a33481 --- /dev/null +++ b/tests/integration/targets/nd_local_user/tasks/main.yml @@ -0,0 +1,350 @@ +# Test code for the ND modules +# Copyright: (c) 2025, Gaspard Micol (@gmicol) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +- name: Test that we have a Nexus Dashboard host, username and password + ansible.builtin.fail: + msg: 'Please define the following variables: ansible_host, ansible_user and ansible_password.' + when: ansible_host is not defined or ansible_user is not defined or ansible_password is not defined + +- name: Set vars + ansible.builtin.set_fact: + nd_info: &nd_info + output_level: '{{ api_key_output_level | default("debug") }}' + +- name: Ensure local users do not exist before test starts + cisco.nd.nd_local_user: + <<: *nd_info + login_id: "{{ item }}" + state: absent + loop: + - ansible_local_user + - ansible_local_user_2 + +# CREATE +- name: Create local user (check mode) + cisco.nd.nd_local_user: &create_local_user + <<: *nd_info + email: ansibleuser@example.com + login_id: ansible_local_user + first_name: Ansible first name + last_name: Ansible last name + user_password: ansibleLocalUserPassword1% + reuse_limitation: 20 + time_interval_limitation: 10 + security_domains: + - name: all + roles: + - observer + - support_engineer + remote_id_claim: ansible_remote_user + remote_user_authorization: true + state: present + check_mode: true + register: cm_create_local_user + +- name: Create local user (normal mode) + cisco.nd.nd_local_user: + <<: *create_local_user + register: nm_create_local_user + +- name: Create a second local user with minimum configuration + cisco.nd.nd_local_user: + <<: *nd_info + login_id: ansible_local_user_2 + user_password: ansibleLocalUser2Password1% + security_domains: + - name: all + state: present + register: nm_create_local_user_2 + +- name: Verify local user creation tasks + ansible.builtin.assert: + that: + - cm_create_local_user is changed + - cm_create_local_user.previous == {} + - cm_create_local_user.current == cm_create_local_user.proposed + - cm_create_local_user.current.email == "ansibleuser@example.com" + - cm_create_local_user.current.firstName == "Ansible first name" + - cm_create_local_user.current.lastName == "Ansible last name" + - cm_create_local_user.current.loginID == "ansible_local_user" + - cm_create_local_user.current.passwordPolicy.reuseLimitation == 20 + - cm_create_local_user.current.passwordPolicy.timeIntervalLimitation == 10 + - cm_create_local_user.current.rbac.domains.all.roles.0 == "observer" + - cm_create_local_user.current.rbac.domains.all.roles.1 == "support-engineer" + - cm_create_local_user.current.remoteIDClaim == "ansible_remote_user" + - cm_create_local_user.current.xLaunch == true + - nm_create_local_user is changed + - nm_create_local_user.previous == {} + - nm_create_local_user.proposed.email == "ansibleuser@example.com" + - nm_create_local_user.proposed.firstName == "Ansible first name" + - nm_create_local_user.proposed.lastName == "Ansible last name" + - nm_create_local_user.proposed.loginID == "ansible_local_user" + - nm_create_local_user.proposed.passwordPolicy.reuseLimitation == 20 + - nm_create_local_user.proposed.passwordPolicy.timeIntervalLimitation == 10 + - nm_create_local_user.proposed.rbac.domains.all.roles.0 == "observer" + - nm_create_local_user.proposed.rbac.domains.all.roles.1 == "support-engineer" + - nm_create_local_user.proposed.remoteIDClaim == "ansible_remote_user" + - nm_create_local_user.proposed.xLaunch == true + - nm_create_local_user.current.email == "ansibleuser@example.com" + - nm_create_local_user.current.firstName == "Ansible first name" + - nm_create_local_user.current.lastName == "Ansible last name" + - nm_create_local_user.current.loginID == "ansible_local_user" + - nm_create_local_user.current.passwordPolicy.reuseLimitation == 20 + - nm_create_local_user.current.passwordPolicy.timeIntervalLimitation == 10 + - nm_create_local_user.current.rbac.domains.all.roles.0 == "observer" + - nm_create_local_user.current.rbac.domains.all.roles.1 == "support-engineer" + - nm_create_local_user.current.remoteIDClaim == "ansible_remote_user" + - nm_create_local_user.current.xLaunch == true + - nm_create_local_user_2 is changed + - nm_create_local_user_2.previous == {} + - nm_create_local_user_2.proposed.loginID == "ansible_local_user_2" + - nm_create_local_user_2.proposed.rbac.domains.all.roles == [] + - nm_create_local_user_2.current.loginID == "ansible_local_user_2" + - nm_create_local_user_2.current.rbac.domains.all.roles == [] + +# UPDATE +- name: Update all ansible_local_user's attributes (check mode) + cisco.nd.nd_local_user: &update_first_local_user + <<: *create_local_user + email: updatedansibleuser@example.com + first_name: Updated Ansible first name + last_name: Updated Ansible last name + user_password: updatedAnsibleLocalUserPassword1% + reuse_limitation: 25 + time_interval_limitation: 15 + security_domains: + - name: all + roles: super_admin + remote_id_claim: "" + remote_user_authorization: false + state: present + check_mode: true + register: cm_update_local_user + +- name: Update local user (normal mode) + cisco.nd.nd_local_user: + <<: *update_first_local_user + register: nm_update_local_user + +- name: Update all ansible_local_user_2's attributes except password + cisco.nd.nd_local_user: &update_second_local_user + <<: *nd_info + email: secondansibleuser@example.com + login_id: ansible_local_user_2 + first_name: Second Ansible first name + last_name: Second Ansible last name + reuse_limitation: 20 + time_interval_limitation: 10 + security_domains: + - name: all + roles: fabric_admin + remote_id_claim: ansible_remote_user_2 + remote_user_authorization: true + state: present + register: nm_update_local_user_2 + +- name: Update all ansible_local_user_2's attributes except password again (idempotency) + cisco.nd.nd_local_user: + <<: *update_second_local_user + register: nm_update_local_user_2_again + +- name: Verify local user update tasks + ansible.builtin.assert: + that: + - cm_update_local_user is changed + - cm_update_local_user.previous.email == "ansibleuser@example.com" + - cm_update_local_user.previous.firstName == "Ansible first name" + - cm_update_local_user.previous.lastName == "Ansible last name" + - cm_update_local_user.previous.loginID == "ansible_local_user" + - cm_update_local_user.previous.passwordPolicy.reuseLimitation == 20 + - cm_update_local_user.previous.passwordPolicy.timeIntervalLimitation == 10 + - cm_update_local_user.previous.rbac.domains.all.roles.0 == "observer" + - cm_update_local_user.previous.rbac.domains.all.roles.1 == "support-engineer" + - cm_update_local_user.previous.remoteIDClaim == "ansible_remote_user" + - cm_update_local_user.previous.xLaunch == true + - cm_update_local_user.current == cm_update_local_user.proposed + - cm_update_local_user.current.email == "updatedansibleuser@example.com" + - cm_update_local_user.current.firstName == "Updated Ansible first name" + - cm_update_local_user.current.lastName == "Updated Ansible last name" + - cm_update_local_user.current.loginID == "ansible_local_user" + - cm_update_local_user.current.passwordPolicy.reuseLimitation == 25 + - cm_update_local_user.current.passwordPolicy.timeIntervalLimitation == 15 + - cm_update_local_user.current.rbac.domains.all.roles.0 == "super-admin" + - cm_update_local_user.current.remoteIDClaim == "" + - cm_update_local_user.current.xLaunch == false + - nm_update_local_user is changed + - nm_update_local_user.previous.email == "ansibleuser@example.com" + - nm_update_local_user.previous.firstName == "Ansible first name" + - nm_update_local_user.previous.lastName == "Ansible last name" + - nm_update_local_user.previous.loginID == "ansible_local_user" + - nm_update_local_user.previous.passwordPolicy.reuseLimitation == 20 + - nm_update_local_user.previous.passwordPolicy.timeIntervalLimitation == 10 + - nm_update_local_user.previous.rbac.domains.all.roles.0 == "observer" + - nm_update_local_user.previous.rbac.domains.all.roles.1 == "support-engineer" + - nm_update_local_user.previous.remoteIDClaim == "ansible_remote_user" + - nm_update_local_user.previous.xLaunch == true + - nm_update_local_user.proposed.email == "updatedansibleuser@example.com" + - nm_update_local_user.proposed.firstName == "Updated Ansible first name" + - nm_update_local_user.proposed.lastName == "Updated Ansible last name" + - nm_update_local_user.proposed.loginID == "ansible_local_user" + - nm_update_local_user.proposed.passwordPolicy.reuseLimitation == 25 + - nm_update_local_user.proposed.passwordPolicy.timeIntervalLimitation == 15 + - nm_update_local_user.proposed.rbac.domains.all.roles.0 == "super-admin" + - nm_update_local_user.proposed.remoteIDClaim == "" + - nm_update_local_user.proposed.xLaunch == false + - nm_update_local_user.current.email == "updatedansibleuser@example.com" + - nm_update_local_user.current.firstName == "Updated Ansible first name" + - nm_update_local_user.current.lastName == "Updated Ansible last name" + - nm_update_local_user.current.loginID == "ansible_local_user" + - nm_update_local_user.current.passwordPolicy.reuseLimitation == 25 + - nm_update_local_user.current.passwordPolicy.timeIntervalLimitation == 15 + - nm_update_local_user.current.rbac.domains.all.roles.0 == "super-admin" + - nm_update_local_user.current.remoteIDClaim == "" + - nm_update_local_user.current.xLaunch == false + - nm_update_local_user_2 is changed + - nm_update_local_user_2.previous.loginID == "ansible_local_user_2" + - nm_update_local_user_2.previous.passwordPolicy.reuseLimitation == 0 + - nm_update_local_user_2.previous.passwordPolicy.timeIntervalLimitation == 0 + - nm_update_local_user_2.previous.rbac.domains.all.roles == [] + - nm_update_local_user_2.previous.xLaunch == false + - nm_update_local_user_2.proposed.email == "secondansibleuser@example.com" + - nm_update_local_user_2.proposed.firstName == "Second Ansible first name" + - nm_update_local_user_2.proposed.lastName == "Second Ansible last name" + - nm_update_local_user_2.proposed.loginID == "ansible_local_user_2" + - nm_update_local_user_2.proposed.passwordPolicy.reuseLimitation == 20 + - nm_update_local_user_2.proposed.passwordPolicy.timeIntervalLimitation == 10 + - nm_update_local_user_2.proposed.rbac.domains.all.roles.0 == "fabric-admin" + - nm_update_local_user_2.proposed.remoteIDClaim == "ansible_remote_user_2" + - nm_update_local_user_2.proposed.xLaunch == true + - nm_update_local_user_2.current.email == "secondansibleuser@example.com" + - nm_update_local_user_2.current.firstName == "Second Ansible first name" + - nm_update_local_user_2.current.lastName == "Second Ansible last name" + - nm_update_local_user_2.current.loginID == "ansible_local_user_2" + - nm_update_local_user_2.current.passwordPolicy.reuseLimitation == 20 + - nm_update_local_user_2.current.passwordPolicy.timeIntervalLimitation == 10 + - nm_update_local_user_2.current.rbac.domains.all.roles.0 == "fabric-admin" + - nm_update_local_user_2.current.remoteIDClaim == "ansible_remote_user_2" + - nm_update_local_user_2.current.xLaunch == true + - nm_update_local_user_2_again.previous.email == "secondansibleuser@example.com" + - nm_update_local_user_2_again.previous.firstName == "Second Ansible first name" + - nm_update_local_user_2_again.previous.lastName == "Second Ansible last name" + - nm_update_local_user_2_again.previous.loginID == "ansible_local_user_2" + - nm_update_local_user_2_again.previous.passwordPolicy.reuseLimitation == 20 + - nm_update_local_user_2_again.previous.passwordPolicy.timeIntervalLimitation == 10 + - nm_update_local_user_2_again.previous.rbac.domains.all.roles.0 == "fabric-admin" + - nm_update_local_user_2_again.previous.remoteIDClaim == "ansible_remote_user_2" + - nm_update_local_user_2_again.previous.xLaunch == true + - nm_update_local_user_2_again.proposed.email == "secondansibleuser@example.com" + - nm_update_local_user_2_again.proposed.firstName == "Second Ansible first name" + - nm_update_local_user_2_again.proposed.lastName == "Second Ansible last name" + - nm_update_local_user_2_again.proposed.loginID == "ansible_local_user_2" + - nm_update_local_user_2_again.proposed.passwordPolicy.reuseLimitation == 20 + - nm_update_local_user_2_again.proposed.passwordPolicy.timeIntervalLimitation == 10 + - nm_update_local_user_2_again.proposed.rbac.domains.all.roles.0 == "fabric-admin" + - nm_update_local_user_2_again.proposed.remoteIDClaim == "ansible_remote_user_2" + - nm_update_local_user_2_again.proposed.xLaunch == true + - nm_update_local_user_2_again.current.email == "secondansibleuser@example.com" + - nm_update_local_user_2_again.current.firstName == "Second Ansible first name" + - nm_update_local_user_2_again.current.lastName == "Second Ansible last name" + - nm_update_local_user_2_again.current.loginID == "ansible_local_user_2" + - nm_update_local_user_2_again.current.passwordPolicy.reuseLimitation == 20 + - nm_update_local_user_2_again.current.passwordPolicy.timeIntervalLimitation == 10 + - nm_update_local_user_2_again.current.rbac.domains.all.roles.0 == "fabric-admin" + - nm_update_local_user_2_again.current.remoteIDClaim == "ansible_remote_user_2" + - nm_update_local_user_2_again.current.xLaunch == true + + +# QUERY +- name: Query all local users + cisco.nd.nd_local_user: + <<: *nd_info + state: query + register: query_all_local_users + +- name: Query specific local user + cisco.nd.nd_local_user: + <<: *nd_info + login_id: ansible_local_user + state: query + register: query_local_user + +- name: query non-existent local user + cisco.nd.nd_local_user: + <<: *nd_info + login_id: non_existent_local_user + state: query + register: query_non_existent + +- name: Verify local user query tasks + ansible.builtin.assert: + that: + - query_all_local_users is not changed + - query_all_local_users.current.localusers | length >= 2 + - query_local_user is not changed + - query_local_user.current.email == "updatedansibleuser@example.com" + - query_local_user.current.firstName == "Updated Ansible first name" + - query_local_user.current.lastName == "Updated Ansible last name" + - query_local_user.current.loginID == "ansible_local_user" + - query_local_user.current.passwordPolicy.reuseLimitation == 25 + - query_local_user.current.passwordPolicy.timeIntervalLimitation == 15 + - query_local_user.current.rbac.domains.all.roles.0 == "super-admin" + - query_local_user.current.xLaunch == false + - query_non_existent is not changed + - query_non_existent.current == {} + +# DELETE +- name: Delete local user by name (check mode) + cisco.nd.nd_local_user: &delete_local_user + <<: *nd_info + login_id: ansible_local_user + state: absent + check_mode: true + register: cm_delete_local_user + +- name: Delete local user by name (normal mode) + cisco.nd.nd_local_user: + <<: *delete_local_user + register: nm_delete_local_user + +- name: Delete local user again (idempotency test) + cisco.nd.nd_local_user: + <<: *delete_local_user + register: nm_delete_local_user_again + +- name: Verify local user deletion tasks + ansible.builtin.assert: + that: + - cm_delete_local_user is changed + - cm_delete_local_user.previous.email == "updatedansibleuser@example.com" + - cm_delete_local_user.previous.firstName == "Updated Ansible first name" + - cm_delete_local_user.previous.lastName == "Updated Ansible last name" + - cm_delete_local_user.previous.loginID == "ansible_local_user" + - cm_delete_local_user.previous.passwordPolicy.reuseLimitation == 25 + - cm_delete_local_user.previous.passwordPolicy.timeIntervalLimitation == 15 + - cm_delete_local_user.previous.rbac.domains.all.roles.0 == "super-admin" + - cm_delete_local_user.previous.xLaunch == false + - cm_delete_local_user.current == cm_delete_local_user.proposed == {} + - nm_delete_local_user is changed + - nm_delete_local_user.previous.email == "updatedansibleuser@example.com" + - nm_delete_local_user.previous.firstName == "Updated Ansible first name" + - nm_delete_local_user.previous.lastName == "Updated Ansible last name" + - nm_delete_local_user.previous.loginID == "ansible_local_user" + - nm_delete_local_user.previous.passwordPolicy.reuseLimitation == 25 + - nm_delete_local_user.previous.passwordPolicy.timeIntervalLimitation == 15 + - nm_delete_local_user.previous.rbac.domains.all.roles.0 == "super-admin" + - nm_delete_local_user.previous.xLaunch == false + - nm_delete_local_user.current == nm_delete_local_user.proposed == {} + - nm_delete_local_user_again is not changed + - nm_delete_local_user_again.current == nm_delete_local_user_again.proposed == nm_delete_local_user_again.previous == {} + +# CLEAN UP +- name: Ensure local users do not exist + cisco.nd.nd_local_user: + <<: *nd_info + login_id: "{{ item }}" + state: absent + loop: + - ansible_local_user + - ansible_local_user_2