diff --git a/changes/491.changed b/changes/491.changed new file mode 100644 index 000000000..d4947b52e --- /dev/null +++ b/changes/491.changed @@ -0,0 +1 @@ +Fixed tenant names and introduced tag for multisite. \ No newline at end of file diff --git a/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py b/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py index cc25aa1b1..816552054 100644 --- a/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py +++ b/nautobot_ssot/integrations/aci/diffsync/adapters/aci.py @@ -83,11 +83,16 @@ def load_tenants(self): for _tenant in tenant_list: if not _tenant["name"] in PLUGIN_CFG.get("ignore_tenants"): tenant_name = f"{self.tenant_prefix}:{_tenant['name']}" + if ":mso" in _tenant.get("annotation").lower(): # pylint: disable=simplifiable-if-statement + _msite_tag = True + else: + _msite_tag = False new_tenant = self.tenant( name=tenant_name, description=_tenant["description"], comments=PLUGIN_CFG.get("comments", ""), site_tag=self.site, + msite_tag=_msite_tag, ) self.add(new_tenant) @@ -210,15 +215,19 @@ def load_ipaddresses(self): vrf_tenant = f"{self.tenant_prefix}:{bd_value['vrf_tenant']}" else: vrf_tenant = None + if bd_value.get("tenant") == "mgmt": + _namespace = "Global" + else: + _namespace = vrf_tenant or tenant_name for subnet in bd_value["subnets"]: prefix = ip_network(subnet[0], strict=False).with_prefixlen self.load_subnet_as_prefix( prefix=prefix, - namespace=tenant_name, + namespace=_namespace, site=self.site, vrf=bd_value["vrf"], vrf_tenant=vrf_tenant, - tenant=tenant_name, + tenant=vrf_tenant or tenant_name, ) new_ipaddress = self.ip_address( address=subnet[0], @@ -227,8 +236,8 @@ def load_ipaddresses(self): description=f"ACI Bridge Domain: {bd_key}", device=None, interface=None, - tenant=tenant_name, - namespace=tenant_name, + tenant=vrf_tenant or tenant_name, + namespace=_namespace, site=self.site, site_tag=self.site, ) @@ -241,7 +250,7 @@ def load_ipaddresses(self): self.add(new_ipaddress) else: self.job.logger.warning( - "Duplicate DiffSync IPAddress Object found and has not been loaded.", + f"Duplicate DiffSync IPAddress Object found: {new_ipaddress.address} in Tenant {new_ipaddress.tenant} and has not been loaded.", ) def load_prefixes(self): @@ -255,15 +264,19 @@ def load_prefixes(self): vrf_tenant = f"{self.tenant_prefix}:{bd_value['vrf_tenant']}" else: vrf_tenant = None - if tenant_name not in PLUGIN_CFG.get("ignore_tenants"): + if bd_value.get("tenant") == "mgmt": + _namespace = "Global" + else: + _namespace = vrf_tenant or tenant_name + if bd_value.get("tenant") not in PLUGIN_CFG.get("ignore_tenants"): for subnet in bd_value["subnets"]: new_prefix = self.prefix( prefix=str(ip_network(subnet[0], strict=False)), - namespace=tenant_name, + namespace=_namespace, status="Active", site=self.site, description=f"ACI Bridge Domain: {bd_key}", - tenant=tenant_name, + tenant=vrf_tenant or tenant_name, vrf=bd_value["vrf"] if bd_value.get("vrf") != "" else None, vrf_tenant=vrf_tenant, site_tag=self.site, @@ -282,7 +295,7 @@ def load_prefixes(self): self.add(new_prefix) else: self.job.logger.warning( - "Duplicate DiffSync Prefix Object found and has not been loaded.", + f"Duplicate DiffSync Prefix Object found {new_prefix.prefix} in Namespace {new_prefix.namespace} and has not been loaded.", ) def load_devicetypes(self): diff --git a/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py b/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py index d3cf370c6..bb811719a 100644 --- a/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py +++ b/nautobot_ssot/integrations/aci/diffsync/adapters/nautobot.py @@ -99,7 +99,11 @@ def load_tenants(self): """Method to load Tenants from Nautobot.""" for nbtenant in Tenant.objects.filter(tags=self.site_tag): _tenant = self.tenant( - name=nbtenant.name, description=nbtenant.description, comments=nbtenant.comments, site_tag=self.site + name=nbtenant.name, + description=nbtenant.description, + comments=nbtenant.comments, + site_tag=self.site, + msite_tag=nbtenant.tags.filter(name="ACI_MULTISITE").exists(), ) self.add(_tenant) diff --git a/nautobot_ssot/integrations/aci/diffsync/client.py b/nautobot_ssot/integrations/aci/diffsync/client.py index 9cd94fc2b..55e7f408b 100644 --- a/nautobot_ssot/integrations/aci/diffsync/client.py +++ b/nautobot_ssot/integrations/aci/diffsync/client.py @@ -1,18 +1,25 @@ """All interactions with ACI.""" # pylint: disable=too-many-lines, too-many-instance-attributes, too-many-arguments # pylint: disable=invalid-name - -import sys import logging -from datetime import datetime -from datetime import timedelta import re +import sys +from copy import deepcopy +from datetime import datetime, timedelta from ipaddress import ip_network + import requests import urllib3 -from .utils import tenant_from_dn, ap_from_dn, node_from_dn, pod_from_dn, fex_id_from_dn, interface_from_dn - +from .utils import ( + ap_from_dn, + bd_from_dn, + fex_id_from_dn, + interface_from_dn, + node_from_dn, + pod_from_dn, + tenant_from_dn, +) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -132,6 +139,7 @@ def get_tenants(self) -> list: { "name": data["fvTenant"]["attributes"]["name"], "description": data["fvTenant"]["attributes"]["descr"], + "annotation": data["fvTenant"]["attributes"].get("annotation", ""), } for data in resp.json()["imdata"] ] @@ -327,45 +335,67 @@ def get_vrfs(self, tenant: str) -> list: ] return vrf_list - def get_bds(self, tenant: str) -> dict: + def get_bds(self, tenant: str = "all") -> dict: """Return Bridge Domains and Subnets from the Cisco APIC.""" + # TODO: rewrite using one API call -> https:///api/node/class/fvBD.json?query-target=subtree&target-subtree-class=fvBD,fvRsCtx,fvSubnet if tenant == "all": - resp = self._get("/api/node/class/fvBD.json") + resp = self._get( + "/api/node/class/fvBD.json?query-target=subtree&target-subtree-class=fvBD,fvRsCtx,fvSubnet" + ) else: - resp = self._get(f"/api/node/mo/uni/tn-{tenant}.json?query-target=children&target-subtree-class=fvBD") - + resp = self._get( + f"/api/node/mo/uni/tn-{tenant}.json?query-target=children&target-subtree-class=fvBD" + ) # test this bd_dict = {} + bd_dict_schema = { + "name": "", + "tenant": "", + "description": "", + "vrf": None, + "vrf_tenant": None, + "subnets": [], + } for data in resp.json()["imdata"]: - bd_dict.setdefault(data["fvBD"]["attributes"]["name"], {}) - bd_dict[data["fvBD"]["attributes"]["name"]]["tenant"] = tenant_from_dn(data["fvBD"]["attributes"]["dn"]) - bd_dict[data["fvBD"]["attributes"]["name"]]["description"] = data["fvBD"]["attributes"]["descr"] - bd_dict[data["fvBD"]["attributes"]["name"]]["unicast_routing"] = data["fvBD"]["attributes"]["unicastRoute"] - bd_dict[data["fvBD"]["attributes"]["name"]]["mac"] = data["fvBD"]["attributes"]["mac"] - bd_dict[data["fvBD"]["attributes"]["name"]]["l2unicast"] = data["fvBD"]["attributes"]["unkMacUcastAct"] - - for key, value in bd_dict.items(): - # get the containing VRF - resp = self._get( - f"/api/node/mo/uni/tn-{value['tenant']}/BD-{key}.json?query-target=children&target-subtree-class=fvRsCtx" - ) - for data in resp.json()["imdata"]: - value["vrf"] = data["fvRsCtx"]["attributes"].get("tnFvCtxName", "default") - vrf_tenant = data["fvRsCtx"]["attributes"].get("tDn", None) + if "fvBD" in data.keys(): + bd_tenant = tenant_from_dn(data["fvBD"]["attributes"]["dn"]) + bd_name = data["fvBD"]["attributes"]["name"] + unique_name = f"{bd_name}:{bd_tenant}" + try: + bd_dict[unique_name] + except KeyError: + bd_dict.setdefault(unique_name, deepcopy(bd_dict_schema)) + bd_dict[unique_name]["tenant"] = tenant_from_dn(data["fvBD"]["attributes"]["dn"]) + bd_dict[unique_name]["name"] = data["fvBD"]["attributes"]["name"] + bd_dict[unique_name]["description"] = data["fvBD"]["attributes"]["descr"] + + elif "fvRsCtx" in data.keys(): + bd_tenant = tenant_from_dn(data["fvRsCtx"]["attributes"]["dn"]) + bd_name = bd_from_dn(data["fvRsCtx"]["attributes"]["dn"]) + unique_name = f"{bd_name}:{bd_tenant}" + try: + bd_dict[unique_name] + except KeyError: + bd_dict.setdefault(unique_name, deepcopy(bd_dict_schema)) + bd_dict[unique_name]["vrf"] = data["fvRsCtx"]["attributes"].get("tnFvCtxName") or "default" + vrf_tenant = data["fvRsCtx"]["attributes"].get("tDn") if vrf_tenant: - value["vrf_tenant"] = tenant_from_dn(vrf_tenant) - else: - value["vrf_tenant"] = None - # get subnets - resp = self._get( - f"/api/node/mo/uni/tn-{value['tenant']}/BD-{key}.json?query-target=children&target-subtree-class=fvSubnet" - ) - subnet_list = [ - (data["fvSubnet"]["attributes"]["ip"], data["fvSubnet"]["attributes"]["scope"]) - for data in resp.json()["imdata"] - ] - for subnet in subnet_list: - value.setdefault("subnets", []) - value["subnets"].append(subnet) + bd_dict[unique_name]["vrf_tenant"] = tenant_from_dn(vrf_tenant) + + elif "fvSubnet" in data.keys(): + bd_tenant = tenant_from_dn(data["fvSubnet"]["attributes"]["dn"]) + bd_name = bd_from_dn(data["fvSubnet"]["attributes"]["dn"]) + unique_name = f"{bd_name}:{bd_tenant}" + try: + bd_dict[unique_name] + except KeyError: + bd_dict.setdefault(unique_name, deepcopy(bd_dict_schema)) + subnet = (data["fvSubnet"]["attributes"]["ip"], data["fvSubnet"]["attributes"]["scope"]) + (bd_dict[unique_name]["subnets"]).append(subnet) + else: + logger.error( + msg=f"Failed to load Bridge Domains data, unexpected response in {data}. Skipping Record..." + ) + continue return bd_dict def get_nodes(self) -> dict: @@ -394,10 +424,10 @@ def get_nodes(self) -> dict: mgmt_addr = f"{node['topSystem']['attributes']['address']}/{ip_network(node['topSystem']['attributes']['tepPool'], strict=False).prefixlen}" else: mgmt_addr = "" - if node["topSystem"]["attributes"]["tepPool"] != "0.0.0.0": # nosec: B104 - subnet = node["topSystem"]["attributes"]["tepPool"] - elif mgmt_addr: + if mgmt_addr: subnet = ip_network(mgmt_addr, strict=False).with_prefixlen + elif node["topSystem"]["attributes"]["tepPool"] != "0.0.0.0": # nosec: B104 + subnet = node["topSystem"]["attributes"]["tepPool"] else: subnet = "" node_id = node["topSystem"]["attributes"]["id"] @@ -424,7 +454,7 @@ def get_nodes(self) -> dict: return node_dict def get_controllers(self) -> dict: - """Return list of Leaf/Spine nodes in the ACI fabric.""" + """Return list of Controller nodes in the ACI fabric.""" resp = self._get('/api/class/fabricNode.json?query-target-filter=eq(fabricNode.role,"controller")') node_dict = {} for node in resp.json()["imdata"]: @@ -447,10 +477,10 @@ def get_controllers(self) -> dict: mgmt_addr = f"{node['topSystem']['attributes']['address']}/{ip_network(node['topSystem']['attributes']['tepPool'], strict=False).prefixlen}" else: mgmt_addr = "" - if node["topSystem"]["attributes"]["tepPool"] != "0.0.0.0": # nosec: B104 - subnet = node["topSystem"]["attributes"]["tepPool"] - elif mgmt_addr: + if mgmt_addr: subnet = ip_network(mgmt_addr, strict=False).with_prefixlen + elif node["topSystem"]["attributes"]["tepPool"] != "0.0.0.0": # nosec: B104 + subnet = node["topSystem"]["attributes"]["tepPool"] else: subnet = "" node_id = node["topSystem"]["attributes"]["id"] diff --git a/nautobot_ssot/integrations/aci/diffsync/device-types/APIC-SERVER-L3.yaml b/nautobot_ssot/integrations/aci/diffsync/device-types/APIC-SERVER-L3.yaml index 0fc92a740..7ff97a4b5 100644 --- a/nautobot_ssot/integrations/aci/diffsync/device-types/APIC-SERVER-L3.yaml +++ b/nautobot_ssot/integrations/aci/diffsync/device-types/APIC-SERVER-L3.yaml @@ -35,6 +35,9 @@ interfaces: - name: ILO type: 1000base-t mgmt_only: true + - name: mgmt0 + type: 10gbase-t + mgmt_only: true - name: LAN-1 type: 10gbase-t mgmt_only: true diff --git a/nautobot_ssot/integrations/aci/diffsync/device-types/APIC-SERVER-M3.yaml b/nautobot_ssot/integrations/aci/diffsync/device-types/APIC-SERVER-M3.yaml index 991216652..df0bca3db 100644 --- a/nautobot_ssot/integrations/aci/diffsync/device-types/APIC-SERVER-M3.yaml +++ b/nautobot_ssot/integrations/aci/diffsync/device-types/APIC-SERVER-M3.yaml @@ -35,6 +35,9 @@ interfaces: - name: ILO type: 1000base-t mgmt_only: true + - name: mgmt0 + type: 10gbase-t + mgmt_only: true - name: LAN-1 type: 10gbase-t mgmt_only: true diff --git a/nautobot_ssot/integrations/aci/diffsync/device-types/N9K-C93180YC-FX3.yaml b/nautobot_ssot/integrations/aci/diffsync/device-types/N9K-C93180YC-FX3.yaml new file mode 100644 index 000000000..da4357b4e --- /dev/null +++ b/nautobot_ssot/integrations/aci/diffsync/device-types/N9K-C93180YC-FX3.yaml @@ -0,0 +1,131 @@ +--- +manufacturer: Cisco +model: N9K-C93180YC-FX3 +part_number: N9K-C93180YC-FX3 +slug: n9k-c93180yc-fx3 +u_height: 1 +is_full_depth: true +console-ports: + - name: console + type: rj-45 +power-ports: + - name: Power Supply 1 + type: iec-60320-c14 + maximum_draw: 425 + allocated_draw: 260 + - name: Power Supply 2 + type: iec-60320-c14 + maximum_draw: 425 + allocated_draw: 260 +interfaces: + - name: Ethernet1/1 + type: 25gbase-x-sfp28 + - name: Ethernet1/2 + type: 25gbase-x-sfp28 + - name: Ethernet1/3 + type: 25gbase-x-sfp28 + - name: Ethernet1/4 + type: 25gbase-x-sfp28 + - name: Ethernet1/5 + type: 25gbase-x-sfp28 + - name: Ethernet1/6 + type: 25gbase-x-sfp28 + - name: Ethernet1/7 + type: 25gbase-x-sfp28 + - name: Ethernet1/8 + type: 25gbase-x-sfp28 + - name: Ethernet1/9 + type: 25gbase-x-sfp28 + - name: Ethernet1/10 + type: 25gbase-x-sfp28 + - name: Ethernet1/11 + type: 25gbase-x-sfp28 + - name: Ethernet1/12 + type: 25gbase-x-sfp28 + - name: Ethernet1/13 + type: 25gbase-x-sfp28 + - name: Ethernet1/14 + type: 25gbase-x-sfp28 + - name: Ethernet1/15 + type: 25gbase-x-sfp28 + - name: Ethernet1/16 + type: 25gbase-x-sfp28 + - name: Ethernet1/17 + type: 25gbase-x-sfp28 + - name: Ethernet1/18 + type: 25gbase-x-sfp28 + - name: Ethernet1/19 + type: 25gbase-x-sfp28 + - name: Ethernet1/20 + type: 25gbase-x-sfp28 + - name: Ethernet1/21 + type: 25gbase-x-sfp28 + - name: Ethernet1/22 + type: 25gbase-x-sfp28 + - name: Ethernet1/23 + type: 25gbase-x-sfp28 + - name: Ethernet1/24 + type: 25gbase-x-sfp28 + - name: Ethernet1/25 + type: 25gbase-x-sfp28 + - name: Ethernet1/26 + type: 25gbase-x-sfp28 + - name: Ethernet1/27 + type: 25gbase-x-sfp28 + - name: Ethernet1/28 + type: 25gbase-x-sfp28 + - name: Ethernet1/29 + type: 25gbase-x-sfp28 + - name: Ethernet1/30 + type: 25gbase-x-sfp28 + - name: Ethernet1/31 + type: 25gbase-x-sfp28 + - name: Ethernet1/32 + type: 25gbase-x-sfp28 + - name: Ethernet1/33 + type: 25gbase-x-sfp28 + - name: Ethernet1/34 + type: 25gbase-x-sfp28 + - name: Ethernet1/35 + type: 25gbase-x-sfp28 + - name: Ethernet1/36 + type: 25gbase-x-sfp28 + - name: Ethernet1/37 + type: 25gbase-x-sfp28 + - name: Ethernet1/38 + type: 25gbase-x-sfp28 + - name: Ethernet1/39 + type: 25gbase-x-sfp28 + - name: Ethernet1/40 + type: 25gbase-x-sfp28 + - name: Ethernet1/41 + type: 25gbase-x-sfp28 + - name: Ethernet1/42 + type: 25gbase-x-sfp28 + - name: Ethernet1/43 + type: 25gbase-x-sfp28 + - name: Ethernet1/44 + type: 25gbase-x-sfp28 + - name: Ethernet1/45 + type: 25gbase-x-sfp28 + - name: Ethernet1/46 + type: 25gbase-x-sfp28 + - name: Ethernet1/47 + type: 25gbase-x-sfp28 + - name: Ethernet1/48 + type: 25gbase-x-sfp28 + - name: Ethernet1/49 + type: 100gbase-x-qsfp28 + - name: Ethernet1/50 + type: 100gbase-x-qsfp28 + - name: Ethernet1/51 + type: 100gbase-x-qsfp28 + - name: Ethernet1/52 + type: 100gbase-x-qsfp28 + - name: Ethernet1/53 + type: 100gbase-x-qsfp28 + - name: Ethernet1/54 + type: 100gbase-x-qsfp28 + - name: mgmt0 + type: 1000base-t + mgmt_only: true diff --git a/nautobot_ssot/integrations/aci/diffsync/device-types/N9K-C93360YC-FX2.yaml b/nautobot_ssot/integrations/aci/diffsync/device-types/N9K-C93360YC-FX2.yaml new file mode 100644 index 000000000..257ad2b83 --- /dev/null +++ b/nautobot_ssot/integrations/aci/diffsync/device-types/N9K-C93360YC-FX2.yaml @@ -0,0 +1,251 @@ +--- +manufacturer: Cisco +model: Nexus 93360YC-FX2 +part_number: N9K-C93360YC-FX2 +slug: cisco-n9k-c93360yc-fx2 +front_image: true +rear_image: true +u_height: 2 +is_full_depth: true +console-ports: + - name: console + type: rj-45 + - name: usb1 + type: usb-a +interfaces: + - name: Ethernet1/1 + type: 25gbase-x-sfp28 + - name: Ethernet1/2 + type: 25gbase-x-sfp28 + - name: Ethernet1/3 + type: 25gbase-x-sfp28 + - name: Ethernet1/4 + type: 25gbase-x-sfp28 + - name: Ethernet1/5 + type: 25gbase-x-sfp28 + - name: Ethernet1/6 + type: 25gbase-x-sfp28 + - name: Ethernet1/7 + type: 25gbase-x-sfp28 + - name: Ethernet1/8 + type: 25gbase-x-sfp28 + - name: Ethernet1/9 + type: 25gbase-x-sfp28 + - name: Ethernet1/10 + type: 25gbase-x-sfp28 + - name: Ethernet1/11 + type: 25gbase-x-sfp28 + - name: Ethernet1/12 + type: 25gbase-x-sfp28 + - name: Ethernet1/13 + type: 25gbase-x-sfp28 + - name: Ethernet1/14 + type: 25gbase-x-sfp28 + - name: Ethernet1/15 + type: 25gbase-x-sfp28 + - name: Ethernet1/16 + type: 25gbase-x-sfp28 + - name: Ethernet1/17 + type: 25gbase-x-sfp28 + - name: Ethernet1/18 + type: 25gbase-x-sfp28 + - name: Ethernet1/19 + type: 25gbase-x-sfp28 + - name: Ethernet1/20 + type: 25gbase-x-sfp28 + - name: Ethernet1/21 + type: 25gbase-x-sfp28 + - name: Ethernet1/22 + type: 25gbase-x-sfp28 + - name: Ethernet1/23 + type: 25gbase-x-sfp28 + - name: Ethernet1/24 + type: 25gbase-x-sfp28 + - name: Ethernet1/25 + type: 25gbase-x-sfp28 + - name: Ethernet1/26 + type: 25gbase-x-sfp28 + - name: Ethernet1/27 + type: 25gbase-x-sfp28 + - name: Ethernet1/28 + type: 25gbase-x-sfp28 + - name: Ethernet1/29 + type: 25gbase-x-sfp28 + - name: Ethernet1/30 + type: 25gbase-x-sfp28 + - name: Ethernet1/31 + type: 25gbase-x-sfp28 + - name: Ethernet1/32 + type: 25gbase-x-sfp28 + - name: Ethernet1/33 + type: 25gbase-x-sfp28 + - name: Ethernet1/34 + type: 25gbase-x-sfp28 + - name: Ethernet1/35 + type: 25gbase-x-sfp28 + - name: Ethernet1/36 + type: 25gbase-x-sfp28 + - name: Ethernet1/37 + type: 25gbase-x-sfp28 + - name: Ethernet1/38 + type: 25gbase-x-sfp28 + - name: Ethernet1/39 + type: 25gbase-x-sfp28 + - name: Ethernet1/40 + type: 25gbase-x-sfp28 + - name: Ethernet1/41 + type: 25gbase-x-sfp28 + - name: Ethernet1/42 + type: 25gbase-x-sfp28 + - name: Ethernet1/43 + type: 25gbase-x-sfp28 + - name: Ethernet1/44 + type: 25gbase-x-sfp28 + - name: Ethernet1/45 + type: 25gbase-x-sfp28 + - name: Ethernet1/46 + type: 25gbase-x-sfp28 + - name: Ethernet1/47 + type: 25gbase-x-sfp28 + - name: Ethernet1/48 + type: 25gbase-x-sfp28 + - name: Ethernet1/49 + type: 25gbase-x-sfp28 + - name: Ethernet1/50 + type: 25gbase-x-sfp28 + - name: Ethernet1/51 + type: 25gbase-x-sfp28 + - name: Ethernet1/52 + type: 25gbase-x-sfp28 + - name: Ethernet1/53 + type: 25gbase-x-sfp28 + - name: Ethernet1/54 + type: 25gbase-x-sfp28 + - name: Ethernet1/55 + type: 25gbase-x-sfp28 + - name: Ethernet1/56 + type: 25gbase-x-sfp28 + - name: Ethernet1/57 + type: 25gbase-x-sfp28 + - name: Ethernet1/58 + type: 25gbase-x-sfp28 + - name: Ethernet1/59 + type: 25gbase-x-sfp28 + - name: Ethernet1/60 + type: 25gbase-x-sfp28 + - name: Ethernet1/61 + type: 25gbase-x-sfp28 + - name: Ethernet1/62 + type: 25gbase-x-sfp28 + - name: Ethernet1/63 + type: 25gbase-x-sfp28 + - name: Ethernet1/64 + type: 25gbase-x-sfp28 + - name: Ethernet1/65 + type: 25gbase-x-sfp28 + - name: Ethernet1/66 + type: 25gbase-x-sfp28 + - name: Ethernet1/67 + type: 25gbase-x-sfp28 + - name: Ethernet1/68 + type: 25gbase-x-sfp28 + - name: Ethernet1/69 + type: 25gbase-x-sfp28 + - name: Ethernet1/70 + type: 25gbase-x-sfp28 + - name: Ethernet1/71 + type: 25gbase-x-sfp28 + - name: Ethernet1/72 + type: 25gbase-x-sfp28 + - name: Ethernet1/73 + type: 25gbase-x-sfp28 + - name: Ethernet1/74 + type: 25gbase-x-sfp28 + - name: Ethernet1/75 + type: 25gbase-x-sfp28 + - name: Ethernet1/76 + type: 25gbase-x-sfp28 + - name: Ethernet1/77 + type: 25gbase-x-sfp28 + - name: Ethernet1/78 + type: 25gbase-x-sfp28 + - name: Ethernet1/79 + type: 25gbase-x-sfp28 + - name: Ethernet1/80 + type: 25gbase-x-sfp28 + - name: Ethernet1/81 + type: 25gbase-x-sfp28 + - name: Ethernet1/82 + type: 25gbase-x-sfp28 + - name: Ethernet1/83 + type: 25gbase-x-sfp28 + - name: Ethernet1/84 + type: 25gbase-x-sfp28 + - name: Ethernet1/85 + type: 25gbase-x-sfp28 + - name: Ethernet1/86 + type: 25gbase-x-sfp28 + - name: Ethernet1/87 + type: 25gbase-x-sfp28 + - name: Ethernet1/88 + type: 25gbase-x-sfp28 + - name: Ethernet1/89 + type: 25gbase-x-sfp28 + - name: Ethernet1/90 + type: 25gbase-x-sfp28 + - name: Ethernet1/91 + type: 25gbase-x-sfp28 + - name: Ethernet1/92 + type: 25gbase-x-sfp28 + - name: Ethernet1/93 + type: 25gbase-x-sfp28 + - name: Ethernet1/94 + type: 25gbase-x-sfp28 + - name: Ethernet1/95 + type: 25gbase-x-sfp28 + - name: Ethernet1/96 + type: 25gbase-x-sfp28 + - name: Ethernet1/97 + type: 100gbase-x-qsfp28 + - name: Ethernet1/98 + type: 100gbase-x-qsfp28 + - name: Ethernet1/99 + type: 100gbase-x-qsfp28 + - name: Ethernet1/100 + type: 100gbase-x-qsfp28 + - name: Ethernet1/101 + type: 100gbase-x-qsfp28 + - name: Ethernet1/102 + type: 100gbase-x-qsfp28 + - name: Ethernet1/103 + type: 100gbase-x-qsfp28 + - name: Ethernet1/104 + type: 100gbase-x-qsfp28 + - name: Ethernet1/105 + type: 100gbase-x-qsfp28 + - name: Ethernet1/106 + type: 100gbase-x-qsfp28 + - name: Ethernet1/107 + type: 100gbase-x-qsfp28 + - name: Ethernet1/108 + type: 100gbase-x-qsfp28 + - name: mgmt0 + type: 1000base-t + mgmt_only: true +module-bays: + - name: PS1 + label: Power Supply 1 + position: '1' + - name: PS2 + label: Power Supply 2 + position: '2' + - name: Fan 1 + position: '1' + - name: Fan 2 + position: '2' + - name: Fan 3 + position: '3' + - name: Fan 4 + position: '4' + - name: Fan 5 + position: '5' diff --git a/nautobot_ssot/integrations/aci/diffsync/device-types/N9K-C93600CD-GX.yaml b/nautobot_ssot/integrations/aci/diffsync/device-types/N9K-C93600CD-GX.yaml new file mode 100644 index 000000000..ba7f00d93 --- /dev/null +++ b/nautobot_ssot/integrations/aci/diffsync/device-types/N9K-C93600CD-GX.yaml @@ -0,0 +1,91 @@ +--- +manufacturer: Cisco +model: Nexus 93600CD-GX +part_number: N9K-C93600CD-GX +slug: cisco-n9k-c93600cd-gx +u_height: 1 +is_full_depth: true +console-ports: + - name: console + type: rj-45 +interfaces: + - name: Ethernet1/1 + type: 100gbase-x-qsfp28 + - name: Ethernet1/2 + type: 100gbase-x-qsfp28 + - name: Ethernet1/3 + type: 100gbase-x-qsfp28 + - name: Ethernet1/4 + type: 100gbase-x-qsfp28 + - name: Ethernet1/5 + type: 100gbase-x-qsfp28 + - name: Ethernet1/6 + type: 100gbase-x-qsfp28 + - name: Ethernet1/7 + type: 100gbase-x-qsfp28 + - name: Ethernet1/8 + type: 100gbase-x-qsfp28 + - name: Ethernet1/9 + type: 100gbase-x-qsfp28 + - name: Ethernet1/10 + type: 100gbase-x-qsfp28 + - name: Ethernet1/11 + type: 100gbase-x-qsfp28 + - name: Ethernet1/12 + type: 100gbase-x-qsfp28 + - name: Ethernet1/13 + type: 100gbase-x-qsfp28 + - name: Ethernet1/14 + type: 100gbase-x-qsfp28 + - name: Ethernet1/15 + type: 100gbase-x-qsfp28 + - name: Ethernet1/16 + type: 100gbase-x-qsfp28 + - name: Ethernet1/17 + type: 100gbase-x-qsfp28 + - name: Ethernet1/18 + type: 100gbase-x-qsfp28 + - name: Ethernet1/19 + type: 100gbase-x-qsfp28 + - name: Ethernet1/20 + type: 100gbase-x-qsfp28 + - name: Ethernet1/21 + type: 100gbase-x-qsfp28 + - name: Ethernet1/22 + type: 100gbase-x-qsfp28 + - name: Ethernet1/23 + type: 100gbase-x-qsfp28 + - name: Ethernet1/24 + type: 100gbase-x-qsfp28 + - name: Ethernet1/25 + type: 100gbase-x-qsfp28 + - name: Ethernet1/26 + type: 100gbase-x-qsfp28 + - name: Ethernet1/27 + type: 100gbase-x-qsfp28 + - name: Ethernet1/28 + type: 100gbase-x-qsfp28 + - name: Ethernet1/29 + type: 400gbase-x-qsfpdd + - name: Ethernet1/30 + type: 400gbase-x-qsfpdd + - name: Ethernet1/31 + type: 400gbase-x-qsfpdd + - name: Ethernet1/32 + type: 400gbase-x-qsfpdd + - name: Ethernet1/33 + type: 400gbase-x-qsfpdd + - name: Ethernet1/34 + type: 400gbase-x-qsfpdd + - name: Ethernet1/35 + type: 400gbase-x-qsfpdd + - name: Ethernet1/36 + type: 400gbase-x-qsfpdd + - name: mgmt0 + type: 1000base-t + mgmt_only: true +module-bays: + - name: PS1 + position: '1' + - name: PS2 + position: '2' diff --git a/nautobot_ssot/integrations/aci/diffsync/models/base.py b/nautobot_ssot/integrations/aci/diffsync/models/base.py index 14c271167..539501aa6 100644 --- a/nautobot_ssot/integrations/aci/diffsync/models/base.py +++ b/nautobot_ssot/integrations/aci/diffsync/models/base.py @@ -9,12 +9,13 @@ class Tenant(DiffSyncModel): _modelname = "tenant" _identifiers = ("name",) - _attributes = ("description", "comments", "site_tag") + _attributes = ("description", "comments", "site_tag", "msite_tag") name: str description: Optional[str] comments: Optional[str] site_tag: str + msite_tag: bool class Vrf(DiffSyncModel): diff --git a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py index 86e72d7f5..0eb02cd45 100644 --- a/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py +++ b/nautobot_ssot/integrations/aci/diffsync/models/nautobot.py @@ -1,6 +1,7 @@ """Nautobot Models for Cisco ACI integration with SSoT app.""" import logging +from django.db import IntegrityError from django.contrib.contenttypes.models import ContentType from nautobot.tenancy.models import Tenant as OrmTenant from nautobot.dcim.models import DeviceType as OrmDeviceType @@ -8,11 +9,11 @@ from nautobot.dcim.models import InterfaceTemplate as OrmInterfaceTemplate from nautobot.dcim.models import Interface as OrmInterface from nautobot.dcim.models import Location, LocationType +from nautobot.dcim.models import Manufacturer from nautobot.ipam.models import IPAddress as OrmIPAddress from nautobot.ipam.models import Namespace, IPAddressToInterface from nautobot.ipam.models import Prefix as OrmPrefix from nautobot.ipam.models import VRF as OrmVrf -from nautobot.dcim.models import Manufacturer from nautobot.extras.models import Role, Status, Tag from nautobot_ssot.integrations.aci.diffsync.models.base import ( Tenant, @@ -38,20 +39,20 @@ class NautobotTenant(Tenant): def create(cls, diffsync, ids, attrs): """Create Tenant object in Nautobot.""" _tenant = OrmTenant(name=ids["name"], description=attrs["description"], comments=attrs["comments"]) + if attrs["msite_tag"]: + _tenant.tags.add(Tag.objects.get(name="ACI_MULTISITE")) _tenant.tags.add(Tag.objects.get(name=PLUGIN_CFG.get("tag"))) _tenant.tags.add(Tag.objects.get(name=attrs["site_tag"])) _tenant.validated_save() - Namespace.objects.create(name=ids["name"]) + Namespace.objects.get_or_create(name=ids["name"]) return super().create(ids=ids, diffsync=diffsync, attrs=attrs) def update(self, attrs): """Update Tenant object in Nautobot.""" _tenant = OrmTenant.objects.get(name=self.name) - if attrs.get("description"): - _tenant.description = attrs["description"] - if attrs.get("comments"): - _tenant.comments = attrs["comments"] + _tenant.description = attrs.get("description", "") + _tenant.comments = attrs.get("comments", "") _tenant.validated_save() return super().update(attrs) @@ -347,23 +348,43 @@ def create(cls, diffsync, ids, attrs): intf = None if attrs["device"] and attrs["interface"]: try: - intf = OrmInterface.objects.get(name=_interface, device__name=_device) + intf = OrmInterface.objects.get( + name=_interface, device__name=_device, device__location__name=ids["site"] + ) except OrmInterface.DoesNotExist: - diffsync.job.logger.warning(f"{_device} missing interface {_interface} to assign {ids['address']}") + diffsync.job.logger.warning(f"{_device} missing interface {_interface} to assign {ids['address']}.") + except OrmInterface.MultipleObjectsReturned: + diffsync.job.logger.warning(f"Found Multiple {_interface} in {_device} to assign {ids['address']}.") if ids["tenant"]: - tenant_name = OrmTenant.objects.get(name=ids["tenant"]) + _tenant = OrmTenant.objects.get(name=ids["tenant"]) else: - tenant_name = None + _tenant = None + try: + _namespace = Namespace.objects.get(name=ids["namespace"]) + _parent = OrmPrefix.objects.get(prefix=attrs["prefix"], namespace=_namespace) + except Namespace.DoesNotExist: + diffsync.job.logger.warning(f"{ids['namespace']} missing Namespace to assign IP address: {ids['address']}") + return None + except OrmPrefix.DoesNotExist: + diffsync.job.logger.warning( + f"{attrs['prefix']} missing Parent Prefix to assign IP address: {ids['address']}" + ) + return None + try: + _ipaddress = OrmIPAddress.objects.create( + address=ids["address"], + status=Status.objects.get(name=attrs["status"]), + description=attrs["description"], + namespace=_namespace, + parent=_parent, + tenant=_tenant, + ) + except IntegrityError: + diffsync.job.logger.warning( + f"Unable to create IP Address {ids['address']}. Duplicate Address or Parent Prefix: {attrs['prefix']} in Namespace: {ids['namespace']}" + ) + return None - namespace = Namespace.objects.get(name=ids["namespace"]) - _ipaddress = OrmIPAddress.objects.create( - address=ids["address"], - status=Status.objects.get(name=attrs["status"]), - description=attrs["description"], - namespace=namespace, - parent=OrmPrefix.objects.get(prefix=attrs["prefix"], namespace=namespace), - tenant=tenant_name, - ) if intf: mapping = IPAddressToInterface.objects.create(ip_address=_ipaddress, interface=intf) mapping.validated_save() @@ -382,13 +403,19 @@ def create(cls, diffsync, ids, attrs): def update(self, attrs): """Update IPAddress object in Nautobot.""" - _ipaddress = OrmIPAddress.objects.get(address=self.address) + _ipaddress = OrmIPAddress.objects.get( + address=self.address, tenant__name=self.tenant, parent__namespace__name=self.namespace + ) if attrs.get("description"): _ipaddress.description = attrs["description"] if attrs.get("tenant"): _ipaddress.tenant = OrmTenant.objects.get(name=self.tenant) if attrs.get("device") and attrs.get("interface"): - intf = OrmInterface.objects.get(name=attrs["interface"], device__name=attrs["device"]) + intf = OrmInterface.objects.get( + name=attrs["interface"], + device__name=attrs["device"], + device__location__name=self.site, + ) mapping = IPAddressToInterface.objects.create(ip_address=_ipaddress, interface=intf) mapping.validated_save() if attrs.get("status"): @@ -419,8 +446,11 @@ def create(cls, diffsync, ids, attrs): try: vrf_tenant = OrmTenant.objects.get(name=attrs["vrf_tenant"]) except OrmTenant.DoesNotExist: - diffsync.job.logger.warning(f"Tenant {attrs['vrf_tenant']} not found for VRF {attrs['vrf']}") + diffsync.job.logger.warning( + f"Tenant {attrs['vrf_tenant']} not found for VRF while creating Prefix: {ids['prefix']}" + ) vrf_tenant = None + return None if ids["vrf"] and vrf_tenant: try: @@ -430,7 +460,7 @@ def create(cls, diffsync, ids, attrs): vrf = None else: vrf = None - _prefix = OrmPrefix.objects.create( + _prefix, created = OrmPrefix.objects.get_or_create( prefix=ids["prefix"], status=Status.objects.get(name=attrs["status"]), description=attrs["description"], @@ -438,6 +468,12 @@ def create(cls, diffsync, ids, attrs): tenant=OrmTenant.objects.get(name=attrs["vrf_tenant"]), location=Location.objects.get(name=ids["site"], location_type=LocationType.objects.get(name="Site")), ) + + if not created: + diffsync.job.logger.warning( + f"Prefix: {_prefix.prefix} duplicate in Namespace: {_prefix.namespace.name}. Skipping .." + ) + return None if vrf: _prefix.vrfs.add(vrf) _prefix.tags.add(Tag.objects.get(name=PLUGIN_CFG.get("tag"))) @@ -479,7 +515,7 @@ def delete(self): _prefix = OrmPrefix.objects.get( prefix=self.prefix, tenant=tenant, - vrf=OrmVrf.objects.get(name=self.vrf, tenant=vrf_tenant), + vrfs=OrmVrf.objects.get(name=self.vrf, tenant=vrf_tenant), ) self.diffsync.objects_to_delete["prefix"].append(_prefix) # pylint: disable=protected-access return self diff --git a/nautobot_ssot/integrations/aci/diffsync/utils.py b/nautobot_ssot/integrations/aci/diffsync/utils.py index 3f3bbe2e2..c9bf072d3 100644 --- a/nautobot_ssot/integrations/aci/diffsync/utils.py +++ b/nautobot_ssot/integrations/aci/diffsync/utils.py @@ -40,10 +40,22 @@ def tenant_from_dn(dn): def ap_from_dn(dn): """Match an ACI Application Profile in the Distinguished Name (DN).""" - pattern = "ap-[A-Za-z0-9\-]+" # noqa: W605 # pylint: disable=anomalous-backslash-in-string + pattern = "ap-[A-Za-z0-9\-\_]+" # noqa: W605 # pylint: disable=anomalous-backslash-in-string return re.search(pattern, dn).group().replace("ap-", "", 1).rstrip("/") +def bd_from_dn(dn): + """Match an ACI Bridge Domain in the Distinguished Name (DN).""" + pattern = "BD-[A-Za-z0-9\-\_]+" # noqa: W605 # pylint: disable=anomalous-backslash-in-string + return re.search(pattern, dn).group().replace("BD-", "", 1).rstrip("/") + + +def epg_from_dn(dn): + """Match an ACI Endpoint Group in the Distinguished Name (DN).""" + pattern = "epg-[A-Za-z0-9\-\_]+" # noqa: W605 # pylint: disable=anomalous-backslash-in-string + return re.search(pattern, dn).group().replace("epg-", "", 1).rstrip("/") + + def load_yamlfile(filename): """Load a YAML file to a Dict.""" with open(filename, "r", encoding="utf-8") as fn: diff --git a/nautobot_ssot/integrations/aci/jobs.py b/nautobot_ssot/integrations/aci/jobs.py index b013544df..e31a3f37f 100644 --- a/nautobot_ssot/integrations/aci/jobs.py +++ b/nautobot_ssot/integrations/aci/jobs.py @@ -2,13 +2,13 @@ from django.templatetags.static import static from django.urls import reverse -from diffsync import DiffSyncFlags from nautobot.core.settings_funcs import is_truthy from nautobot.extras.jobs import BooleanVar, ChoiceVar, Job -from nautobot_ssot.jobs.base import DataMapping, DataSource + +from nautobot_ssot.integrations.aci.constant import PLUGIN_CFG from nautobot_ssot.integrations.aci.diffsync.adapters.aci import AciAdapter from nautobot_ssot.integrations.aci.diffsync.adapters.nautobot import NautobotAdapter -from nautobot_ssot.integrations.aci.constant import PLUGIN_CFG +from nautobot_ssot.jobs.base import DataMapping, DataSource name = "Cisco ACI SSoT" # pylint: disable=invalid-name, abstract-method @@ -49,13 +49,6 @@ class Meta: # pylint: disable=too-few-public-methods data_source_icon = static("nautobot_ssot_aci/aci.png") description = "Sync information from ACI to Nautobot" - def __init__(self): - """Initialize ExampleYAMLDataSource.""" - super().__init__() - self.diffsync_flags = ( - self.diffsync_flags | DiffSyncFlags.SKIP_UNMATCHED_DST # pylint: disable=unsupported-binary-operation - ) - @classmethod def data_mappings(cls): """Shows mapping of models between ACI and Nautobot.""" diff --git a/nautobot_ssot/integrations/aci/signals.py b/nautobot_ssot/integrations/aci/signals.py index a29bf8333..4cd29f5fc 100644 --- a/nautobot_ssot/integrations/aci/signals.py +++ b/nautobot_ssot/integrations/aci/signals.py @@ -38,6 +38,10 @@ def aci_create_tag(apps, **kwargs): name=PLUGIN_CFG.get("tag_down"), color=PLUGIN_CFG.get("tag_down_color"), ) + tag.objects.update_or_create( + name="ACI_MULTISITE", + color="03a9f4", + ) apics = PLUGIN_CFG.get("apics") for key in apics: if ("SITE" in key or "STAGE" in key) and not tag.objects.filter(name=apics[key]).exists(): @@ -62,12 +66,14 @@ def aci_create_site(apps, **kwargs): Device = apps.get_model("dcim", "Device") Site = apps.get_model("dcim", "Location") Prefix = apps.get_model("ipam", "Prefix") + Vlan = apps.get_model("ipam", "VLAN") location_type = apps.get_model("dcim", "LocationType") status = apps.get_model("extras", "Status") apics = PLUGIN_CFG.get("apics") loc_type = location_type.objects.update_or_create(name="Site")[0] loc_type.content_types.add(ContentType.objects.get_for_model(Device)) loc_type.content_types.add(ContentType.objects.get_for_model(Prefix)) + loc_type.content_types.add(ContentType.objects.get_for_model(Vlan)) active_status = status.objects.update_or_create(name="Active")[0] for key in apics: if "SITE" in key: diff --git a/nautobot_ssot/tests/aci/test_api.py b/nautobot_ssot/tests/aci/test_api.py index 858184ee2..04adb0487 100644 --- a/nautobot_ssot/tests/aci/test_api.py +++ b/nautobot_ssot/tests/aci/test_api.py @@ -2,7 +2,8 @@ # pylint: disable=import-outside-toplevel, invalid-name import unittest -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch + from nautobot_ssot.integrations.aci.diffsync.client import AciApi, RequestHTTPError @@ -29,8 +30,8 @@ def test_get_tenants(self, mocked_login, mocked_handle_request): mock_fvTenant.status_code = 200 mock_fvTenant.json.return_value = { "imdata": [ - {"fvTenant": {"attributes": {"name": "test_tenant_1", "descr": "test_desc_1"}}}, - {"fvTenant": {"attributes": {"name": "test_tenant_2", "descr": "test_desc_2"}}}, + {"fvTenant": {"attributes": {"name": "test_tenant_1", "descr": "test_desc_1", "annotation": ""}}}, + {"fvTenant": {"attributes": {"name": "test_tenant_2", "descr": "test_desc_2", "annotation": ""}}}, ] } @@ -40,8 +41,8 @@ def test_get_tenants(self, mocked_login, mocked_handle_request): self.assertEqual( self.aci_obj.get_tenants(), [ - {"name": "test_tenant_1", "description": "test_desc_1"}, - {"name": "test_tenant_2", "description": "test_desc_2"}, + {"name": "test_tenant_1", "description": "test_desc_1", "annotation": ""}, + {"name": "test_tenant_2", "description": "test_desc_2", "annotation": ""}, ], ) @@ -470,7 +471,25 @@ def test_get_bds(self, mocked_login, mocked_handle_request): "mac": "00:22:BD:F8:19:FF", "unkMacUcastAct": "proxy", } - } + }, + }, + { + "fvRsCtx": { + "attributes": { + "dn": "uni/tn-ntc-chatops/BD-Vlan100_Web/rsctx", + "tnFvCtxName": "vrf1", + "tDn": "uni/tn-ntc-chatops/ctx-vrf1", + } + }, + }, + { + "fvSubnet": { + "attributes": { + "dn": "uni/tn-ntc-chatops/BD-Vlan100_Web/subnet-[10.1.1.1/24]", + "ip": "10.1.1.1/24", + "scope": "public", + } + }, }, { "fvBD": { @@ -482,62 +501,48 @@ def test_get_bds(self, mocked_login, mocked_handle_request): "mac": "00:22:BD:F8:19:FF", "unkMacUcastAct": "proxy", } - } + }, + }, + { + "fvRsCtx": { + "attributes": { + "dn": "uni/tn-ntc-chatops/BD-Vlan101_App/rsctx", + "tnFvCtxName": "vrf2", + "tDn": "uni/tn-ntc-chatops/ctx-vrf1", + } + }, + }, + { + "fvSubnet": { + "attributes": { + "dn": "uni/tn-ntc-chatops/BD-Vlan101_App/subnet-[10.2.2.2/24]", + "ip": "10.2.2.2/24", + "scope": "public", + } + }, }, ] } - mocked_fvRsCtx_1 = Mock() - mocked_fvRsCtx_1.status_code = 200 - mocked_fvRsCtx_1.json.return_value = { - "imdata": [{"fvRsCtx": {"attributes": {"tnFvCtxName": "vrf1", "tDn": "uni/tn-ntc-chatops/ctx-vrf1"}}}] - } - - mocked_fvRsCtx_2 = Mock() - mocked_fvRsCtx_2.status_code = 200 - mocked_fvRsCtx_2.json.return_value = { - "imdata": [{"fvRsCtx": {"attributes": {"tnFvCtxName": "vrf2", "tDn": "uni/tn-ntc-chatops/ctx-vrf1"}}}] - } - - mocked_fvSubnet_1 = Mock() - mocked_fvSubnet_1.status_code = 200 - mocked_fvSubnet_1.json.return_value = { - "imdata": [{"fvSubnet": {"attributes": {"ip": "10.1.1.1/24", "scope": "public"}}}] - } - - mocked_fvSubnet_2 = Mock() - mocked_fvSubnet_2.status_code = 200 - mocked_fvSubnet_2.json.return_value = { - "imdata": [{"fvSubnet": {"attributes": {"ip": "10.2.2.2/24", "scope": "public"}}}] - } - mocked_login.return_value = self.mock_login mocked_handle_request.side_effect = [ mocked_fvBD, - mocked_fvRsCtx_1, - mocked_fvSubnet_1, - mocked_fvRsCtx_2, - mocked_fvSubnet_2, ] expected_data = { - "Vlan100_Web": { + "Vlan100_Web:ntc-chatops": { + "name": "Vlan100_Web", "tenant": "ntc-chatops", "vrf_tenant": "ntc-chatops", "description": "WEB", - "unicast_routing": "yes", - "mac": "00:22:BD:F8:19:FF", - "l2unicast": "proxy", "vrf": "vrf1", "subnets": [("10.1.1.1/24", "public")], }, - "Vlan101_App": { + "Vlan101_App:ntc-chatops": { + "name": "Vlan101_App", "tenant": "ntc-chatops", "vrf_tenant": "ntc-chatops", "description": "APP", - "unicast_routing": "yes", - "mac": "00:22:BD:F8:19:FF", - "l2unicast": "proxy", "vrf": "vrf2", "subnets": [("10.2.2.2/24", "public")], }, @@ -728,7 +733,7 @@ def test_get_controllers(self, mocked_login, mocked_handle_request): "fabric_ip": "10.0.0.1", "site": "ACI", "pod_id": "1", - "subnet": "10.0.0.0/24", + "subnet": "10.1.1.0/24", "oob_ip": "10.1.1.1/24", "uptime": "05:22:43:18.000", },