Skip to content

Commit e38fee8

Browse files
committed
[minor_change] Add nd_local_user as a new network resource module for Nexus Dashboard v4.1.0 and higher.
1 parent e4676e0 commit e38fee8

File tree

6 files changed

+1011
-51
lines changed

6 files changed

+1011
-51
lines changed

plugins/module_utils/constants.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@
157157
"restart",
158158
"delete",
159159
"update",
160+
"merged",
161+
"replaced",
162+
"overridden",
163+
"deleted",
164+
"gathered",
160165
)
161166

162167
INTERFACE_FLOW_RULES_TYPES_MAPPING = {"port_channel": "PORTCHANNEL", "physical": "PHYSICAL", "l3out_sub_interface": "L3_SUBIF", "l3out_svi": "SVI"}
@@ -168,3 +173,12 @@
168173
ND_REST_KEYS_TO_SANITIZE = ["metadata"]
169174

170175
ND_SETUP_NODE_DEPLOYMENT_TYPE = {"physical": "cimc", "virtual": "vnode"}
176+
177+
USER_ROLES_MAPPING = {
178+
"fabric_admin": "fabric-admin",
179+
"observer": "observer",
180+
"super_admin": "super-admin",
181+
"support_engineer": "support-engineer",
182+
"approver": "approver",
183+
"designer": "designer",
184+
}

plugins/module_utils/nd.py

Lines changed: 23 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from ansible.module_utils.basic import json
1919
from ansible.module_utils.basic import env_fallback
2020
from ansible.module_utils.six import PY3
21-
from ansible.module_utils.six.moves import filterfalse
2221
from ansible.module_utils.six.moves.urllib.parse import urlencode
2322
from ansible.module_utils._text import to_native, to_text
2423
from ansible.module_utils.connection import Connection
@@ -73,53 +72,27 @@ def cmp(a, b):
7372

7473

7574
def issubset(subset, superset):
76-
"""Recurse through nested dictionary and compare entries"""
75+
"""Recurse through a nested dictionary and check if it is a subset of another."""
7776

78-
# Both objects are the same object
79-
if subset is superset:
80-
return True
81-
82-
# Both objects are identical
83-
if subset == superset:
84-
return True
85-
86-
# Both objects have a different type
87-
if isinstance(subset) is not isinstance(superset):
77+
if type(subset) is not type(superset):
8878
return False
8979

80+
if not isinstance(subset, dict):
81+
if isinstance(subset, list):
82+
return all(item in superset for item in subset)
83+
return subset == superset
84+
9085
for key, value in subset.items():
91-
# Ignore empty values
9286
if value is None:
93-
return True
87+
continue
9488

95-
# Item from subset is missing from superset
9689
if key not in superset:
9790
return False
9891

99-
# Item has different types in subset and superset
100-
if isinstance(superset.get(key)) is not isinstance(value):
101-
return False
92+
superset_value = superset.get(key)
10293

103-
# Compare if item values are subset
104-
if isinstance(value, dict):
105-
if not issubset(superset.get(key), value):
106-
return False
107-
elif isinstance(value, list):
108-
try:
109-
# NOTE: Fails for lists of dicts
110-
if not set(value) <= set(superset.get(key)):
111-
return False
112-
except TypeError:
113-
# Fall back to exact comparison for lists of dicts
114-
diff = list(filterfalse(lambda i: i in value, superset.get(key))) + list(filterfalse(lambda j: j in superset.get(key), value))
115-
if diff:
116-
return False
117-
elif isinstance(value, set):
118-
if not value <= superset.get(key):
119-
return False
120-
else:
121-
if not value == superset.get(key):
122-
return False
94+
if not issubset(value, superset_value):
95+
return False
12396

12497
return True
12598

@@ -252,7 +225,7 @@ def request(
252225
if file is not None:
253226
info = conn.send_file_request(method, uri, file, data, None, file_key, file_ext)
254227
else:
255-
if data:
228+
if data is not None:
256229
info = conn.send_request(method, uri, json.dumps(data))
257230
else:
258231
info = conn.send_request(method, uri)
@@ -310,6 +283,8 @@ def request(
310283
self.fail_json(msg="ND Error: {0}".format(self.error.get("message")), data=data, info=info)
311284
self.error = payload
312285
if "code" in payload:
286+
if self.status == 404 and ignore_not_found_error:
287+
return {}
313288
self.fail_json(msg="ND Error {code}: {message}".format(**payload), data=data, info=info, payload=payload)
314289
elif "messages" in payload and len(payload.get("messages")) > 0:
315290
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):
506481
if not self.existing and self.sent:
507482
return True
508483

509-
existing = self.existing
510-
sent = self.sent
484+
existing = deepcopy(self.existing)
485+
sent = deepcopy(self.sent)
511486

512487
for key in unwanted:
513488
if isinstance(key, str):
514489
if key in existing:
515-
try:
516-
del existing[key]
517-
except KeyError:
518-
pass
519-
try:
520-
del sent[key]
521-
except KeyError:
522-
pass
490+
del existing[key]
491+
if key in sent:
492+
del sent[key]
523493
elif isinstance(key, list):
524494
key_path, last = key[:-1], key[-1]
525495
try:
526496
existing_parent = reduce(dict.get, key_path, existing)
527-
del existing_parent[last]
497+
if existing_parent is not None:
498+
del existing_parent[last]
528499
except KeyError:
529500
pass
530501
try:
531502
sent_parent = reduce(dict.get, key_path, sent)
532-
del sent_parent[last]
503+
if sent_parent is not None:
504+
del sent_parent[last]
533505
except KeyError:
534506
pass
535507
return not issubset(sent, existing)
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright: (c) 2025, Gaspard Micol (@gmicol) <[email protected]>
4+
5+
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
6+
7+
from __future__ import absolute_import, division, print_function
8+
9+
__metaclass__ = type
10+
11+
from copy import deepcopy
12+
13+
14+
# Custom NDConfigCollection Exceptions
15+
class NDConfigCollectionError(Exception):
16+
"""Base exception for NDConfigCollection errors."""
17+
pass
18+
19+
20+
class NDConfigNotFoundError(NDConfigCollectionError, KeyError):
21+
"""Raised when a configuration is not found by its identifier."""
22+
pass
23+
24+
25+
class NDIdentifierMismatchError(NDConfigCollectionError, ValueError):
26+
"""Raised when an identifier in a config does not match the expected key."""
27+
pass
28+
29+
30+
class InvalidNDConfigError(NDConfigCollectionError, TypeError):
31+
"""Raised when a provided config is not a dictionary or is missing the identifier key."""
32+
pass
33+
34+
35+
# TODO: Maybe add a get_diff_config function
36+
# TODO: Handle multiple identifiers
37+
# TODO: Add descriptions
38+
# NOTE: New data structure for ND Network Resource Module
39+
class NDConfigCollection:
40+
def __init__(self, identifier_key, data=None):
41+
if not isinstance(identifier_key, str):
42+
raise TypeError("identifier_key must be a string.")
43+
self.identifier_key = identifier_key
44+
self.config_collection = {}
45+
46+
if data is not None:
47+
if isinstance(data, list):
48+
self.list_view = data
49+
elif isinstance(data, dict):
50+
self.config_collection = data
51+
else:
52+
raise TypeError("data must be a list of dicts or dict of configs.")
53+
54+
@property
55+
def list_view(self):
56+
return [v.copy() for v in self.config_collection.values()]
57+
58+
@list_view.setter
59+
def list_view(self, new_list):
60+
if not isinstance(new_list, list):
61+
raise TypeError("list_view must be set to a list.")
62+
63+
new_dict = {}
64+
for item in new_list:
65+
if not isinstance(item, dict):
66+
raise TypeError("All items in list_view must be dicts.")
67+
if self.identifier_key not in item:
68+
raise InvalidNDConfigError(f"Missing '{self.identifier_key}' in item: {item}")
69+
70+
key = item[self.identifier_key]
71+
new_dict[key] = item.copy()
72+
self.config_collection = new_dict
73+
74+
# Basic Operations
75+
def replace(self, config):
76+
if not isinstance(config, dict):
77+
raise InvalidNDConfigError("Config must be a dict.")
78+
if self.identifier_key not in config:
79+
raise InvalidNDConfigError(f"Missing '{self.identifier_key}' in config: {config}")
80+
81+
key = config[self.identifier_key]
82+
self.config_collection[key] = config.copy()
83+
84+
def merge(self, config):
85+
if not isinstance(config, dict):
86+
raise InvalidNDConfigError("Config must be a dict.")
87+
if self.identifier_key not in config:
88+
raise InvalidNDConfigError(f"Missing '{self.identifier_key}' in config: {config}")
89+
90+
key = config[self.identifier_key]
91+
if key in self.config_collection:
92+
self.config_collection[key].update(config.copy())
93+
else:
94+
self.config_collection[key] = config.copy()
95+
96+
def remove(self, identifier):
97+
if identifier not in self.config_collection:
98+
raise NDConfigNotFoundError(f"Configuration with identifier '{identifier}' not found.")
99+
del self.config_collection[identifier]
100+
101+
def get(self, identifier):
102+
config = self.config_collection.get(identifier)
103+
if config is None:
104+
raise NDConfigNotFoundError(f"Configuration with identifier '{identifier}' not found.")
105+
return config.copy()
106+
107+
# Magic Methods
108+
def __len__(self):
109+
return len(self.config_collection)
110+
111+
def __contains__(self, identifier):
112+
return identifier in self.config_collection
113+
114+
def __iter__(self):
115+
for config in self.config_collection.values():
116+
yield config.copy()
117+
118+
def __getitem__(self, identifier):
119+
return self.get(identifier)
120+
121+
def __setitem__(self, identifier, config):
122+
if not isinstance(config, dict):
123+
raise InvalidNDConfigError("Config must be a dict when setting via __setitem__.")
124+
if self.identifier_key not in config:
125+
raise InvalidNDConfigError(f"Config must contain '{self.identifier_key}' when setting via __setitem__.")
126+
if config[self.identifier_key] != identifier:
127+
raise NDIdentifierMismatchError(
128+
f"Identifier '{identifier}' in key does not match '{self.identifier_key}' value "
129+
f"'{config[self.identifier_key]}' in config."
130+
)
131+
self.replace(config)
132+
133+
def __delitem__(self, identifier):
134+
self.remove(identifier)
135+
136+
def __eq__(self, other):
137+
if not isinstance(other, NDConfigCollection):
138+
# TODO: Make it works for list and dict as well. For now just raise an error.
139+
raise InvalidNDConfigError("Can only do __eq__ with another NDConfigCollection instance.")
140+
141+
if self.identifier_key != other.identifier_key:
142+
return False
143+
144+
return self.config_collection == other.config_collection
145+
146+
def __repr__(self):
147+
return f"NDConfigCollection(identifier_key='{self.identifier_key}', count={len(self)})"
148+
149+
def __ne__(self, other):
150+
return not self.__eq__(other)
151+
152+
# Standard Dictionary-like Views
153+
def keys(self):
154+
return self.config_collection.keys()
155+
156+
def values(self):
157+
for v in self.config_collection.values():
158+
yield v.copy()
159+
160+
def items(self):
161+
for k, v in self.config_collection.items():
162+
yield k, v.copy()
163+
164+
# Utility/Convenience Functions
165+
def clear(self):
166+
self.config_collection.clear()
167+
168+
def find_by_attribute(self, attribute_name, attribute_value):
169+
matching_configs = []
170+
for config in self.values():
171+
if config.get(attribute_name) == attribute_value:
172+
matching_configs.append(config.copy())
173+
return matching_configs
174+
175+
def copy(self):
176+
return NDConfigCollection(self.identifier_key, data=deepcopy(self.config_collection))
177+
178+
def sanitize(self, keys_to_remove=None, values_to_remove=None, recursive=True, remove_none_values=True):
179+
if keys_to_remove is None:
180+
keys_to_remove = []
181+
if values_to_remove is None:
182+
values_to_remove = []
183+
184+
sanitized_collection = self.copy()
185+
for k, v in self.items():
186+
if k in keys_to_remove:
187+
del sanitized_collection[k]
188+
elif v in values_to_remove or (v is None and remove_none_values):
189+
del sanitized_collection[k]
190+
elif isinstance(v, dict) and recursive:
191+
sanitized_collection[k] = self.sanitize(v, keys_to_remove, values_to_remove)
192+
elif isinstance(v, list) and recursive:
193+
for index, item in enumerate(v):
194+
if isinstance(item, dict):
195+
sanitized_collection[k][index] = self.sanitize(item, keys_to_remove, values_to_remove)
196+
return sanitized_collection
197+
198+
def get_diff_identifiers(self, other_collection):
199+
if not isinstance(other_collection, NDConfigCollection):
200+
raise InvalidNDConfigError("Can only do get_removed_identifiers with another NDConfigCollection instance.")
201+
202+
if self.identifier_key != other_collection.identifier_key:
203+
raise NDIdentifierMismatchError(f"Cannot do get_removed_identifiers with another NDConfigCollection with different identifier_key. Expected '{self.identifier_key}', got '{other_collection.identifier_key}'.")
204+
current_identifiers = set(self.config_collection.keys())
205+
other_identifiers = set(other_collection.config_collection.keys())
206+
207+
return list(current_identifiers - other_identifiers)

0 commit comments

Comments
 (0)