Skip to content

Commit 60f2d00

Browse files
committed
[minor_change] Add a new module nd_local_user for local users on Nexus Dashboard v4.1.0 or higher.
1 parent e4676e0 commit 60f2d00

File tree

4 files changed

+563
-0
lines changed

4 files changed

+563
-0
lines changed

plugins/module_utils/constants.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,12 @@
168168
ND_REST_KEYS_TO_SANITIZE = ["metadata"]
169169

170170
ND_SETUP_NODE_DEPLOYMENT_TYPE = {"physical": "cimc", "virtual": "vnode"}
171+
172+
USER_ROLES_MAPPING = {
173+
"fabric_admin": "fabric-admin",
174+
"observer": "observer",
175+
"super_admin": "super-admin",
176+
"support_engineer": "support-engineer",
177+
"approver": "approver",
178+
"designer": "designer",
179+
}

plugins/module_utils/nd.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,10 @@ def request(
317317
if ignore_not_found_error:
318318
return {}
319319
self.fail_json(msg="ND Error: {0}".format(payload["errors"][0]), data=data, info=info, payload=payload)
320+
elif "error" in payload and len(payload.get("error")) > 0:
321+
if ignore_not_found_error:
322+
return {}
323+
self.fail_json(msg="ND Error: {0}".format(payload["error"]), data=data, info=info, payload=payload)
320324
else:
321325
if ignore_not_found_error:
322326
return {}

plugins/modules/nd_local_user.py

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
4+
# Copyright: (c) 2025, Gaspard Micol (@gmicol) <[email protected]>
5+
6+
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
7+
8+
from __future__ import absolute_import, division, print_function
9+
10+
__metaclass__ = type
11+
12+
ANSIBLE_METADATA = {"metadata_version": "1.1", "status": ["preview"], "supported_by": "community"}
13+
14+
DOCUMENTATION = r"""
15+
---
16+
module: nd_local_user
17+
version_added: "1.4.0"
18+
short_description: Manage local users on Nexus Dashboard
19+
description:
20+
- Manage local users on Cisco Nexus Dashboard (ND).
21+
- It supports creating, updating, querying, and deleting local users.
22+
author:
23+
- Gaspard Micol (@gmicol)
24+
options:
25+
email:
26+
description:
27+
- The email address of the local user.
28+
type: str
29+
login_id:
30+
description:
31+
- The login ID of the local user.
32+
type: str
33+
first_name:
34+
description:
35+
- The first name of the local user.
36+
type: str
37+
last_name:
38+
description:
39+
- The last name of the local user.
40+
type: str
41+
user_password:
42+
description:
43+
- The password of the local user.
44+
- Password must have a minimum of 8 characters to a maximum of 64 characters.
45+
- Password must have three of the following; one number, one lower case character, one upper case character, one special character.
46+
type: str
47+
reuse_limitation:
48+
description:
49+
- The number of different passwords a user must use before they can reuse a previous one.
50+
type: int
51+
time_interval_limitation:
52+
description:
53+
- The minimum time period that must pass before a previous password can be reused.
54+
type: int
55+
security_domains:
56+
description:
57+
- The list of Security Domains and Roles for the local user.
58+
type: list
59+
elements: dict
60+
suboptions:
61+
name:
62+
description:
63+
- The name of the Security Domain to which give the local user access.
64+
type: str
65+
required: true
66+
aliases: [ security_domain_name, domain_name ]
67+
roles:
68+
description:
69+
- The Permission Roles of the local user within the Security Domain.
70+
type: list
71+
elements: str
72+
choices: [ fabric_admin, observer, super_admin, support_engineer, approver, designer ]
73+
aliases: [ domains ]
74+
remote_id_claim:
75+
description:
76+
- The remote ID claim of the local user.
77+
type: str
78+
remote_user_authorization:
79+
description:
80+
- To enable/disable the Remote User Authorization of the local user.
81+
- Remote User Authorization is used for signing into Nexus Dashboard when using identity providers that cannot provide authorization claims.
82+
Once this attribute is enabled, the local user ID cannot be used to directly login to Nexus Dashboard.
83+
type: bool
84+
state:
85+
description:
86+
- Use C(present) to create or update a local user.
87+
- Use C(absent) to delete an existing local user.
88+
- Use C(query) for listing all the existing local users or a specific local user if O(login_id) is specified.
89+
type: str
90+
default: present
91+
choices: [ present, absent, query ]
92+
extends_documentation_fragment:
93+
- cisco.nd.modules
94+
- cisco.nd.check_mode
95+
notes:
96+
- This module is only supported on Nexus Dashboard having version 4.1.0 or higher.
97+
- This module is not idempotent when creating or updating a local user object.
98+
"""
99+
100+
EXAMPLES = r"""
101+
- name: Create a new local user
102+
cisco.nd.nd_local_user:
103+
104+
login_id: local_user
105+
first_name: User first name
106+
last_name: User last name
107+
user_password: localUserPassword1%
108+
reuse_limitation: 20
109+
time_interval_limitation: 10
110+
security_domains:
111+
name: all
112+
roles:
113+
- observer
114+
- support_engineer
115+
remote_id_claim: remote_user
116+
remote_user_authorization: true
117+
state: present
118+
register: result
119+
120+
- name: Create local user with minimal configuration
121+
cisco.nd.nd_local_user:
122+
login_id: local_user_min
123+
user_password: localUserMinuser_password
124+
security_domain: all
125+
state: present
126+
127+
- name: Update local user
128+
cisco.nd.nd_local_user:
129+
130+
login_id: local_user
131+
first_name: Updated user first name
132+
last_name: Updated user last name
133+
user_password: updatedLocalUserPassword1%
134+
reuse_limitation: 25
135+
time_interval_limitation: 15
136+
security_domains:
137+
- name: all
138+
roles: super_admin
139+
- name: ansible_domain
140+
roles: observer
141+
roles: super_admin
142+
remote_id_claim: ""
143+
remote_user_authorization: false
144+
state: present
145+
146+
- name: Query an existing local user
147+
cisco.nd.nd_local_user:
148+
login_id: local_user
149+
state: query
150+
register: query_result
151+
152+
- name: Query all local users
153+
cisco.nd.nd_local_user:
154+
state: query
155+
register: all_keys
156+
157+
- name: Delete an local user
158+
cisco.nd.nd_local_user:
159+
login_id: local_user
160+
state: absent
161+
"""
162+
163+
RETURN = r"""
164+
"""
165+
166+
from ansible.module_utils.basic import AnsibleModule
167+
from ansible_collections.cisco.nd.plugins.module_utils.nd import NDModule, nd_argument_spec, sanitize_dict
168+
from ansible_collections.cisco.nd.plugins.module_utils.constants import USER_ROLES_MAPPING
169+
170+
171+
def main():
172+
argument_spec = nd_argument_spec()
173+
argument_spec.update(
174+
email=dict(type="str"),
175+
login_id=dict(type="str"),
176+
first_name=dict(type="str"),
177+
last_name=dict(type="str"),
178+
user_password=dict(type="str", no_log=True),
179+
reuse_limitation=dict(type="int"),
180+
time_interval_limitation=dict(type="int"),
181+
security_domains=dict(
182+
type="list",
183+
elements="dict",
184+
options=dict(
185+
name=dict(type="str", required=True, aliases=["security_domain_name", "domain_name"]),
186+
roles=dict(type="list", elements="str", choices=list(USER_ROLES_MAPPING)),
187+
),
188+
aliases=["domains"],
189+
),
190+
remote_id_claim=dict(type="str"),
191+
remote_user_authorization=dict(type="bool"),
192+
state=dict(type="str", default="present", choices=["present", "absent", "query"]),
193+
)
194+
195+
module = AnsibleModule(
196+
argument_spec=argument_spec,
197+
supports_check_mode=True,
198+
required_if=[
199+
["state", "present", ["login_id"]],
200+
["state", "absent", ["login_id"]],
201+
],
202+
)
203+
204+
nd = NDModule(module)
205+
206+
email = nd.params.get("email")
207+
login_id = nd.params.get("login_id")
208+
first_name = nd.params.get("first_name")
209+
last_name = nd.params.get("last_name")
210+
user_password = nd.params.get("user_password")
211+
reuse_limitation = nd.params.get("reuse_limitation")
212+
time_interval_limitation = nd.params.get("time_interval_limitation")
213+
security_domains = nd.params.get("security_domains")
214+
remote_id_claim = nd.params.get("remote_id_claim")
215+
remote_user_authorization = nd.params.get("remote_user_authorization")
216+
state = nd.params.get("state")
217+
218+
path = "/api/v1/infra/aaa/localUsers"
219+
if login_id:
220+
updated_path = "{0}/{1}".format(path, login_id)
221+
nd.existing = nd.previous = nd.query_obj(path=updated_path, ignore_not_found_error=True)
222+
else:
223+
nd.existing = nd.query_obj(path=path, ignore_not_found_error=True)
224+
225+
if state == "present":
226+
227+
payload = {
228+
"email": email,
229+
"firstName": first_name,
230+
"lastName": last_name,
231+
"loginID": login_id,
232+
"password": user_password,
233+
"remoteIDClaim": remote_id_claim,
234+
"xLaunch": remote_user_authorization,
235+
}
236+
237+
if security_domains:
238+
payload["rbac"] = {
239+
"domains": {
240+
security_domain.get("name"): {
241+
"roles": [USER_ROLES_MAPPING.get(role) for role in security_domain["roles"]] if isinstance(security_domain.get("roles"), list) else [],
242+
}
243+
for security_domain in security_domains
244+
},
245+
}
246+
if reuse_limitation or time_interval_limitation:
247+
payload["passwordPolicy"] = sanitize_dict(
248+
{
249+
"reuseLimitation": reuse_limitation,
250+
"timeIntervalLimitation": time_interval_limitation,
251+
}
252+
)
253+
254+
nd.sanitize(payload)
255+
256+
if not module.check_mode:
257+
if nd.existing:
258+
nd.existing = nd.request(path=updated_path, method="PUT", data=payload)
259+
else:
260+
nd.existing = nd.request(path=path, method="POST", data=payload)
261+
else:
262+
nd.existing = nd.proposed
263+
264+
elif state == "absent":
265+
if nd.existing:
266+
if not module.check_mode:
267+
nd.request(path="{0}/{1}".format(path, login_id), method="DELETE")
268+
nd.existing = {}
269+
270+
nd.exit_json()
271+
272+
273+
if __name__ == "__main__":
274+
main()

0 commit comments

Comments
 (0)