Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions plugins/module_utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
4 changes: 4 additions & 0 deletions plugins/module_utils/nd.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,10 @@ def request(
if ignore_not_found_error:
return {}
self.fail_json(msg="ND Error: {0}".format(payload["errors"][0]), data=data, info=info, payload=payload)
elif "error" in payload and len(payload.get("error")) > 0:
if ignore_not_found_error:
return {}
self.fail_json(msg="ND Error: {0}".format(payload["error"]), data=data, info=info, payload=payload)
else:
if ignore_not_found_error:
return {}
Expand Down
274 changes: 274 additions & 0 deletions plugins/modules/nd_local_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (c) 2025, Gaspard Micol (@gmicol) <[email protected]>

# 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).
- It supports creating, updating, querying, and deleting local users.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- It supports creating, updating, querying, and deleting local users.
- This module supports creating, updating, querying, and deleting local users.

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.
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.
type: str
reuse_limitation:
description:
- The number of different passwords a user must use before they can reuse a previous one.
type: int
time_interval_limitation:
description:
- The minimum time period that must pass before a previous password can be reused.
type: int
security_domains:
description:
- The list of Security Domains and Roles for the local user.
type: list
elements: dict
suboptions:
name:
description:
- The name of the Security Domain to which give the local user access.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please change this to... if it's more meaningful?

  • 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.
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 only supported on Nexus Dashboard having version 4.1.0 or higher.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this note be in the module description instead?

- This module is only supported on ND v4.1.0 and later.

Our ansible-mso modules are worded like this, should we keep it similar?

- This module is not idempotent when creating or updating a local user object.
"""

EXAMPLES = r"""
- name: Create a new local user
cisco.nd.nd_local_user:
email: [email protected]
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a new standard? It's not that I disagree with it, but should we then track this as new thing we want to introduce to all our modules?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something that was introduced with the new nd_api_key module. So I thought this has become the new standard but if not, we should discuss if we should add this creation task with minimum config in the EXAMPLES section.
I think this is interesting to have as it makes visually clearer what attributes are required when creating a new object.
@sajagana, @shrsr, @samiib, @lhercot, @anvitha-jain what do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no preference on this

cisco.nd.nd_local_user:
login_id: local_user_min
user_password: localUserMinuser_password
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use one of the ways? - either camelCase or snake ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

security_domain: all
state: present

- name: Update local user
cisco.nd.nd_local_user:
email: [email protected]
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 an local user
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change to?
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:
updated_path = "{0}/{1}".format(path, login_id)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we rename this to login_id_path or something? think updated is to generic

nd.existing = nd.previous = nd.query_obj(path=updated_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 [],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is empty list required to be set in roles, or should we also sanitize this part?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only way I found to provide an empty list of roles assigned to a security domain. Assigning an None value to "roles" or sanitizing the key will result in an API error.
Also, as providing an empty list works, I wonder if this is a normal behavior or a bug as it seems weird to not assign any role to a local user for a security domain.

}
for security_domain in security_domains
},
}
if reuse_limitation or time_interval_limitation:
payload["passwordPolicy"] = sanitize_dict(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is sanitize_dict() required here when we sanitize the entire payload in the next step?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's required as nd.sanitize() is not recursive and just sanitize one level deep which is not enough in this case. Maybe, we should address this issue in the future so that the nd.sanitize() can be recursive as well

{
"reuseLimitation": reuse_limitation,
"timeIntervalLimitation": time_interval_limitation,
}
)

nd.sanitize(payload)

if not module.check_mode:
if nd.existing:
nd.existing = nd.request(path=updated_path, method="PUT", data=payload)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is PUT op called even for the same configuration?
If yes, shall we leverage get_diff()?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added get_diff() function for pre-checking PUT operation

else:
nd.existing = nd.request(path=path, method="POST", data=payload)
else:
nd.existing = nd.proposed

elif state == "absent":
if nd.existing:
if not module.check_mode:
nd.request(path=updated_path, method="DELETE")
nd.existing = {}

nd.exit_json()


if __name__ == "__main__":
main()
Loading
Loading