Skip to content

Commit f5fb674

Browse files
authored
[minor_change] Add new module nd_site (#47)
1 parent 81ba1b8 commit f5fb674

File tree

5 files changed

+555
-1
lines changed

5 files changed

+555
-1
lines changed

plugins/httpapi/nd.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,10 @@ def login(self, username, password):
121121
json_response = self._response_to_json(response_data)
122122
self.error = dict(code=self.status, message="Authentication failed: {0}".format(json_response))
123123
raise ConnectionError(json.dumps(self._verify_response(response, method, full_path, response_data)))
124-
self.connection._auth = {"Authorization": "Bearer {0}".format(self._response_to_json(response_data).get("token"))}
124+
self.connection._auth = {
125+
"Authorization": "Bearer {0}".format(self._response_to_json(response_data).get("token")),
126+
"Cookie": "AuthCookie={0}".format(self._response_to_json(response_data).get("token")),
127+
}
125128

126129
except ConnectionError as connection_err:
127130
self.connection.queue_message("error", "login() - ConnectionError Exception: {0}".format(connection_err))

plugins/module_utils/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,5 @@
127127
TCP_FLAGS = {"ack": "ACKNOWLEDGEMENT", "est": "ESTABLISHED", "fin": "FINISH", "res": "RESET", "syn": "SYNCHRONIZED"}
128128

129129
EPOCH_DELTA_TYPES = {"latest": 0, "last_15_min": 900, "last_hour": 3600, "last_2_hours": 7200, "last_6_hours": 21600, "last_day": 86400, "last_week": 604800}
130+
131+
SITE_TYPE_MAP = {"aci": "ACI", "dcnm": "DCNM", "third_party": "ThirdParty", "cloud_aci": "CloudACI", "dcnm_ng": "DCNMNG", "ndfc": "NDFC"}

plugins/module_utils/nd.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
88

99
from __future__ import absolute_import, division, print_function
10+
from functools import reduce
1011

1112
__metaclass__ = type
1213

@@ -462,3 +463,41 @@ def check_changed(self):
462463
if "password" in existing:
463464
existing["password"] = self.sent.get("password")
464465
return not issubset(self.sent, existing)
466+
467+
def get_diff(self, unwanted=None):
468+
"""Check if existing payload and sent payload and removing keys that are not required"""
469+
if unwanted is None:
470+
unwanted = []
471+
if not self.existing and self.sent:
472+
return True
473+
474+
existing = self.existing
475+
sent = self.sent
476+
477+
for key in unwanted:
478+
if type(key) is str:
479+
if key in existing:
480+
try:
481+
del existing[key]
482+
except KeyError:
483+
pass
484+
try:
485+
del sent[key]
486+
except KeyError:
487+
pass
488+
elif type(key) is list:
489+
key_path, last = key[:-1], key[-1]
490+
try:
491+
existing_parent = reduce(dict.get, key_path, existing)
492+
del existing_parent[last]
493+
except KeyError:
494+
pass
495+
try:
496+
sent_parent = reduce(dict.get, key_path, sent)
497+
del sent_parent[last]
498+
except KeyError:
499+
pass
500+
return not issubset(sent, existing)
501+
502+
def set_to_empty_string_when_none(self, val):
503+
return val if val is not None else ""

plugins/modules/nd_site.py

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
4+
# Copyright: (c) 2023, Anvitha Jain (@anvitha-jain) <[email protected]>
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+
ANSIBLE_METADATA = {"metadata_version": "1.1", "status": ["preview"], "supported_by": "community"}
12+
13+
DOCUMENTATION = r"""
14+
---
15+
module: nd_site
16+
short_description: Manage sites on Nexus Dashboard.
17+
description:
18+
- Manage sites on Nexus Dashboard which are then used by Nexus Dashboard Orchestrator (NDO) >= 2.2(2d).
19+
author:
20+
- Anvitha Jain (@anvitha-jain)
21+
options:
22+
site_password:
23+
description:
24+
- The password for the APIC.
25+
type: str
26+
site_username:
27+
description:
28+
- The username for the APIC.
29+
type: str
30+
login_domain:
31+
description:
32+
- The AAA login domain for the username of the APIC.
33+
type: str
34+
inband_epg:
35+
description:
36+
- The In-Band Endpoint Group (EPG) used to connect Nexus Dashboard to the fabric.
37+
type: str
38+
site_name:
39+
description:
40+
- The name of the site.
41+
type: str
42+
aliases: [ name, fabric_name ]
43+
# ToDo: To avail the securityDomains feature, ND v4 API update is required.
44+
# security_domains:
45+
# description:
46+
# - The security_domains for this site.
47+
# type: list
48+
# elements: str
49+
latitude:
50+
description:
51+
- The latitude of the location of the site.
52+
type: float
53+
longitude:
54+
description:
55+
- The longitude of the location of the site.
56+
type: float
57+
url:
58+
description:
59+
- The URL to reference the APICs.
60+
type: str
61+
aliases: [ host_name, ip_address, site_url]
62+
site_type:
63+
description:
64+
- The site type of the APICs.
65+
type: str
66+
choices: [ aci, dcnm, third_party, cloud_aci, dcnm_ng, ndfc ]
67+
re_register:
68+
description:
69+
- To modify the APIC parameters (site_username, site_password and login_domain).
70+
- Using this option deletes the existing site and re-creates it.
71+
type: bool
72+
default: false
73+
state:
74+
description:
75+
- Use C(present) or C(absent) for adding or removing.
76+
- Use C(query) for listing an object or multiple objects.
77+
type: str
78+
choices: [ absent, present, query ]
79+
default: present
80+
extends_documentation_fragment: cisco.nd.modules
81+
"""
82+
83+
EXAMPLES = r"""
84+
- name: Add a new site
85+
cisco.nd.nd_site:
86+
url: SiteURL
87+
site_username: SiteUsername
88+
site_password: SitePassword
89+
site_type: aci
90+
location:
91+
latitude: 50.887318
92+
longitude: 4.447084
93+
login_domain: "DefaultAuth"
94+
inband_epg: In-Band-EPG
95+
state: present
96+
delegate_to: localhost
97+
98+
- name: Remove a site
99+
cisco.nd.nd_site:
100+
site_name: north_europe
101+
state: absent
102+
delegate_to: localhost
103+
104+
- name: Query a site
105+
cisco.nd.nd_site:
106+
site_name: north_europe
107+
state: query
108+
delegate_to: localhost
109+
register: query_result
110+
111+
- name: Query all sites
112+
cisco.nd.nd_site:
113+
state: query
114+
delegate_to: localhost
115+
register: query_result
116+
"""
117+
118+
RETURN = r"""
119+
"""
120+
121+
from ansible.module_utils.basic import AnsibleModule
122+
from ansible_collections.cisco.nd.plugins.module_utils.nd import NDModule, nd_argument_spec
123+
from ansible_collections.cisco.nd.plugins.module_utils.constants import SITE_TYPE_MAP
124+
import base64
125+
126+
127+
def main():
128+
argument_spec = nd_argument_spec()
129+
argument_spec.update(
130+
site_password=dict(type="str", no_log=True),
131+
site_username=dict(type="str"),
132+
login_domain=dict(type="str"),
133+
inband_epg=dict(type="str"),
134+
site_name=dict(type="str", aliases=["name", "fabric_name"]),
135+
url=dict(type="str", aliases=["host_name", "ip_address", "site_url"]),
136+
site_type=dict(type="str", choices=list(SITE_TYPE_MAP.keys())),
137+
# ToDo: To avail the securityDomains feature, ND v4 API update is required.
138+
# security_domains=dict(type="list", elements="str"),
139+
latitude=dict(type="float"),
140+
longitude=dict(type="float"),
141+
re_register=dict(type="bool", default=False),
142+
state=dict(type="str", default="present", choices=["absent", "present", "query"]),
143+
)
144+
145+
module = AnsibleModule(
146+
argument_spec=argument_spec,
147+
supports_check_mode=True,
148+
required_if=[
149+
["state", "absent", ["site_name"]],
150+
["state", "present", ["site_name", "site_username", "site_password", "url", "site_type"]],
151+
],
152+
)
153+
154+
nd = NDModule(module)
155+
156+
site_username = nd.params.get("site_username")
157+
158+
site_password = nd.params.get("site_password")
159+
if site_password is not None:
160+
# Make password base64 encoded
161+
site_password = (base64.b64encode(str.encode(site_password))).decode("utf-8")
162+
163+
inband_epg = nd.set_to_empty_string_when_none(nd.params.get("inband_epg"))
164+
if inband_epg != "":
165+
inband_epg = "uni/tn-mgmt/mgmtp-default/inb-{0}".format(inband_epg)
166+
167+
login_domain = nd.set_to_empty_string_when_none(nd.params.get("login_domain"))
168+
site_name = nd.params.get("site_name")
169+
url = nd.params.get("url")
170+
site_type = nd.params.get("site_type")
171+
latitude = nd.set_to_empty_string_when_none(nd.params.get("latitude"))
172+
longitude = nd.set_to_empty_string_when_none(nd.params.get("longitude"))
173+
# ToDo: To avail the securityDomains feature, ND v4 API update is required.
174+
# security_domains = nd.params.get("security_domains")
175+
re_register = nd.params.get("re_register")
176+
state = nd.params.get("state")
177+
178+
path = "/nexus/api/sitemanagement/v4/sites"
179+
site_obj = nd.query_obj(path).get("items")
180+
181+
if site_name:
182+
site_info = next((site_dict for site_dict in site_obj if site_dict.get("spec").get("name") == site_name), None)
183+
if site_info:
184+
site_path = "{0}/{1}".format(path, site_name)
185+
nd.existing = site_info
186+
else:
187+
nd.existing = site_obj
188+
189+
nd.previous = nd.existing
190+
191+
if state == "query":
192+
nd.exit_json()
193+
elif state == "absent":
194+
if nd.existing:
195+
if not module.check_mode:
196+
nd.request(site_path, method="DELETE")
197+
nd.existing = {}
198+
elif state == "present":
199+
site_configuration = {}
200+
if site_type == "aci" or site_type == "cloud_aci":
201+
site_type_param = site_type
202+
if site_type == "cloud_aci":
203+
site_type_param = SITE_TYPE_MAP.get(site_type)
204+
site_configuration.update(
205+
{
206+
site_type_param: {
207+
"InbandEPGDN": inband_epg,
208+
}
209+
}
210+
)
211+
elif site_type == "ndfc" or site_type == "dcnm":
212+
site_configuration.update({site_type: {"fabricName": site_name, "fabricTechnology": "External", "fabricType": "External"}})
213+
214+
payload = {
215+
"spec": {
216+
"name": site_name,
217+
"siteType": SITE_TYPE_MAP.get(site_type),
218+
"host": url,
219+
"userName": site_username,
220+
"password": site_password,
221+
"loginDomain": login_domain,
222+
"latitude": str(latitude),
223+
"longitude": str(longitude),
224+
# ToDo: To avail the securityDomains feature, ND v4 API update is required.
225+
# "securityDomains": security_domains if security_domains is not None else [],
226+
"siteConfig": site_configuration,
227+
},
228+
}
229+
230+
nd.sanitize(payload, collate=True)
231+
232+
if not module.check_mode:
233+
method = "POST"
234+
if re_register:
235+
nd.request(site_path, method="DELETE")
236+
nd.existing = {}
237+
if nd.existing and not re_register:
238+
method = "PUT"
239+
path = site_path
240+
241+
unwanted = [
242+
"metadata",
243+
"status",
244+
["spec", "annotation"],
245+
["spec", "host"],
246+
["spec", "userName"],
247+
["spec", "password"],
248+
["spec", "loginDomain"],
249+
["spec", "useProxy"],
250+
["spec", "secureVerify"],
251+
["spec", "internalOnboard"],
252+
]
253+
if nd.get_diff(unwanted):
254+
nd.existing = nd.request(path, method=method, data=payload)
255+
else:
256+
nd.has_modified = True
257+
258+
nd.existing = nd.proposed
259+
260+
nd.exit_json()
261+
262+
263+
if __name__ == "__main__":
264+
main()

0 commit comments

Comments
 (0)