diff --git a/plugins/module_utils/network/dcnm/dcnm.py b/plugins/module_utils/network/dcnm/dcnm.py index e308fd275..96ec52609 100644 --- a/plugins/module_utils/network/dcnm/dcnm.py +++ b/plugins/module_utils/network/dcnm/dcnm.py @@ -283,6 +283,47 @@ def get_ip_sn_fabric_dict(inventory_data): return ip_fab, sn_fab +def get_ip_fabric_dict(inventory_data): + """ + Maps the switch IP Address/Serial No. in the multisite inventory + data to respective member site fabric name to which it was actually added. + + Parameters: + inventory_data: Fabric inventory data + + Returns: + dict: Switch ip - fabric_name mapping + dict: Switch serial_no - fabric_name mapping + """ + ip_fab = {} + + for device_key in inventory_data.keys(): + ip = inventory_data[device_key].get("ipAddress") + fabric_name = inventory_data[device_key].get("fabricName") + ip_fab.update({ip: fabric_name}) + + return ip_fab + +def get_sn_fabric_dict(inventory_data): + """ + Maps the switch IP Address/Serial No. in the multisite inventory + data to respective member site fabric name to which it was actually added. + + Parameters: + inventory_data: Fabric inventory data + + Returns: + dict: Switch ip - fabric_name mapping + dict: Switch serial_no - fabric_name mapping + """ + sn_fab = {} + + for device_key in inventory_data.keys(): + sn = inventory_data[device_key].get("serialNumber") + fabric_name = inventory_data[device_key].get("fabricName") + sn_fab.update({sn: fabric_name}) + + return sn_fab # sw_elem can be ip_addr, hostname, dns name or serial number. If the given # sw_elem is ip_addr, then it is returned as is. If DNS or hostname then a DNS diff --git a/plugins/modules/dcnm_vrf.py b/plugins/modules/dcnm_vrf.py index 72342eb7e..6c8fa37eb 100644 --- a/plugins/modules/dcnm_vrf.py +++ b/plugins/modules/dcnm_vrf.py @@ -571,8 +571,9 @@ from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( dcnm_get_ip_addr_info, dcnm_get_url, dcnm_send, dcnm_version_supported, - get_fabric_details, get_fabric_inventory_details, get_ip_sn_dict, - get_ip_sn_fabric_dict, validate_list_of_dicts) + get_fabric_details, get_fabric_inventory_details, get_ip_fabric_dict, + get_ip_sn_dict, get_ip_sn_fabric_dict, get_sn_fabric_dict, + validate_list_of_dicts) from ..module_utils.common.log_v2 import Log @@ -661,7 +662,9 @@ def __init__(self, module): self.sn_ip = {value: key for (key, value) in self.ip_sn.items()} self.fabric_data = get_fabric_details(self.module, self.fabric) self.fabric_type = self.fabric_data.get("fabricType") - self.ip_fab, self.sn_fab = get_ip_sn_fabric_dict(self.inventory_data) + # self.ip_fab, self.sn_fab = get_ip_sn_fabric_dict(self.inventory_data) + self.ip_fab = get_ip_fabric_dict(self.inventory_data) + self.sn_fab = get_sn_fabric_dict(self.inventory_data) if self.dcnm_version > 12: self.paths = dcnm_vrf_paths[12] else: @@ -3049,6 +3052,80 @@ def send_to_controller(self, action, verb, path, payload, is_rollback=False): self.log.debug(msg) self.failure(resp) + def update_vrf_attach_fabric_name(self, vrf_attach: dict) -> dict: + """ + # Summary + + For multisite fabrics, replace `vrf_attach.fabric` with the name of + the child fabric returned by `self.sn_fab[vrf_attach.serialNumber]` + + ## params + + - `vrf_attach` + + A `vrf_attach` dictionary containing the following keys: + + - `fabric` : fabric name + - `serialNumber` : switch serial number + """ + method_name = inspect.stack()[0][3] + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + msg = "Received vrf_attach: " + msg += f"{json.dumps(vrf_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + if self.fabric_type != "MFD": + msg = "Early return. " + msg += f"FABRIC_TYPE {self.fabric_type} is not MFD. " + msg += "Returning unmodified vrf_attach." + self.log.debug(msg) + return copy.deepcopy(vrf_attach) + + parent_fabric_name = vrf_attach.get("fabric") + + msg = f"fabric_type: {self.fabric_type}, " + msg += "replacing parent_fabric_name " + msg += f"({parent_fabric_name}) " + msg += "with child fabric name." + self.log.debug(msg) + + serial_number = vrf_attach.get("serialNumber") + + if serial_number is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "Unable to parse serial_number from vrf_attach. " + msg += f"{json.dumps(vrf_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + self.module.fail_json(msg) + + child_fabric_name = self.sn_fab[serial_number] + + if child_fabric_name is None: + msg = f"{self.class_name}.{method_name}: " + msg += f"caller: {caller}. " + msg += "Unable to determine child fabric name for serial_number " + msg += f"{serial_number}." + self.log.debug(msg) + self.module.fail_json(msg) + + msg = f"serial_number: {serial_number}, " + msg += f"child fabric name: {child_fabric_name}. " + self.log.debug(msg) + + vrf_attach["fabric"] = child_fabric_name + + msg += "Updated vrf_attach: " + msg += f"{json.dumps(vrf_attach, indent=4, sort_keys=True)}" + self.log.debug(msg) + + return copy.deepcopy(vrf_attach) + def push_diff_attach(self, is_rollback=False): """ # Summary @@ -3086,6 +3163,8 @@ def push_diff_attach(self, is_rollback=False): msg += f"{json.dumps(vrf_attach, indent=4, sort_keys=True)}" self.log.debug(msg) + vrf_attach = self.update_vrf_attach_fabric_name(vrf_attach) + if "is_deploy" in vrf_attach: del vrf_attach["is_deploy"] if not vrf_attach.get("vrf_lite"): @@ -3164,13 +3243,6 @@ def push_diff_attach(self, is_rollback=False): path = self.paths["GET_VRF"].format(self.fabric) attach_path = path + "/attachments" - # For multisite fabrics, update the fabric name to the child fabric - # containing the switches. - if self.fabric_type == "MFD": - for elem in new_diff_attach_list: - for node in elem["lanAttachList"]: - node["fabric"] = self.sn_fab[node["serialNumber"]] - self.send_to_controller( action, verb, attach_path, new_diff_attach_list, is_rollback ) diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_vrf.json b/tests/unit/modules/dcnm/fixtures/dcnm_vrf.json index 37f60c141..a6f83e3bb 100644 --- a/tests/unit/modules/dcnm/fixtures/dcnm_vrf.json +++ b/tests/unit/modules/dcnm/fixtures/dcnm_vrf.json @@ -6,6 +6,13 @@ "10.10.10.227": "XYZKSJHSMK4", "10.10.10.228": "XYZKSJHSMK5" }, + "mock_sn_fab" : { + "XYZKSJHSMK1": "test_fabric", + "XYZKSJHSMK2": "test_fabric", + "XYZKSJHSMK3": "test_fabric", + "XYZKSJHSMK4": "test_fabric", + "XYZKSJHSMK5": "test_fabric" + }, "playbook_config_input_validation" : [ { "vrf_template": "Default_VRF_Universal", @@ -1423,15 +1430,15 @@ "switchRole": "border" } }, - "fabric_details": { + "fabric_details_orig": { "createdOn": 1613750822779, "deviceType": "n9k", "fabricId": "FABRIC-15", "fabricName": "MS-fabric", "fabricTechnology": "VXLANFabric", "fabricTechnologyFriendly": "VXLAN Fabric", - "fabricType": "MFD", - "fabricTypeFriendly": "Multi-Fabric Domain", + "fabricType": "Switch_Fabric", + "fabricTypeFriendly": "Switch Fabric", "id": 15, "modifiedOn": 1613750822779, "networkExtensionTemplate": "Default_Network_Extension_Universal", @@ -1448,13 +1455,14 @@ "DCI_SUBNET_TARGET_MASK": "30", "DELAY_RESTORE": "300", "FABRIC_NAME": "MS-fabric", - "FABRIC_TYPE": "MFD", - "FF": "MSD", + "FABRIC_TYPE": "Switch_Fabric", + "FF": "Easy_Fabric", "L2_SEGMENT_ID_RANGE": "30000-49000", "L3_PARTITION_ID_RANGE": "50000-59000", "LOOPBACK100_IP_RANGE": "10.10.0.0/24", "MS_LOOPBACK_ID": "100", "MS_UNDERLAY_AUTOCONFIG": "true", + "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", "RP_SERVER_IP": "", "TOR_AUTO_DEPLOY": "false", "default_network": "Default_Network_Universal", @@ -1466,11 +1474,107 @@ }, "provisionMode": "DCNMTopDown", "replicationMode": "IngressReplication", - "templateName": "MSD_Fabric_11_1", + "templateName": "Easy_Fabric", "vrfExtensionTemplate": "Default_VRF_Extension_Universal", "vrfTemplate": "Default_VRF_Universal" }, - "mock_vrf12_object": { + "fabric_details_vxlan": { + "createdOn": 1613750822779, + "deviceType": "n9k", + "fabricId": "FABRIC-15", + "fabricName": "MS-fabric", + "fabricTechnology": "VXLANFabric", + "fabricTechnologyFriendly": "VXLAN Fabric", + "fabricType": "Switch_Fabric", + "fabricTypeFriendly": "Switch Fabric", + "id": 15, + "modifiedOn": 1613750822779, + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "networkTemplate": "Default_Network_Universal", + "nvPairs": { + "ANYCAST_GW_MAC": "2020.0000.00aa", + "BGP_RP_ASN": "", + "BORDER_GWY_CONNECTIONS": "Direct_To_BGWS", + "CLOUDSEC_ALGORITHM": "", + "CLOUDSEC_AUTOCONFIG": "false", + "CLOUDSEC_ENFORCEMENT": "", + "CLOUDSEC_KEY_STRING": "", + "DCI_SUBNET_RANGE": "10.10.1.0/24", + "DCI_SUBNET_TARGET_MASK": "30", + "DELAY_RESTORE": "300", + "FABRIC_NAME": "MS-fabric", + "FABRIC_TYPE": "Switch_Fabric", + "FF": "Easy_Fabric", + "L2_SEGMENT_ID_RANGE": "30000-49000", + "L3_PARTITION_ID_RANGE": "50000-59000", + "LOOPBACK100_IP_RANGE": "10.10.0.0/24", + "MS_LOOPBACK_ID": "100", + "MS_UNDERLAY_AUTOCONFIG": "true", + "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", + "RP_SERVER_IP": "", + "TOR_AUTO_DEPLOY": "false", + "default_network": "Default_Network_Universal", + "default_vrf": "Default_VRF_Universal", + "enableScheduledBackup": "false", + "network_extension_template": "Default_Network_Extension_Universal", + "scheduledTime": "", + "vrf_extension_template": "Default_VRF_Extension_Universal" + }, + "provisionMode": "DCNMTopDown", + "replicationMode": "IngressReplication", + "templateName": "Easy_Fabric", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfTemplate": "Default_VRF_Universal" + }, + "fabric_details": { + "createdOn": 1613750822779, + "deviceType": "n9k", + "fabricId": "FABRIC-15", + "fabricName": "MS-fabric", + "fabricTechnology": "VXLANFabric", + "fabricTechnologyFriendly": "VXLAN Fabric", + "fabricType": "MFD", + "fabricTypeFriendly": "Multi-Fabric Domain", + "id": 15, + "modifiedOn": 1613750822779, + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "networkTemplate": "Default_Network_Universal", + "nvPairs": { + "ANYCAST_GW_MAC": "2020.0000.00aa", + "BGP_RP_ASN": "", + "BORDER_GWY_CONNECTIONS": "Direct_To_BGWS", + "CLOUDSEC_ALGORITHM": "", + "CLOUDSEC_AUTOCONFIG": "false", + "CLOUDSEC_ENFORCEMENT": "", + "CLOUDSEC_KEY_STRING": "", + "DCI_SUBNET_RANGE": "10.10.1.0/24", + "DCI_SUBNET_TARGET_MASK": "30", + "DELAY_RESTORE": "300", + "FABRIC_NAME": "MS-fabric", + "FABRIC_TYPE": "MFD", + "FF": "MSD", + "L2_SEGMENT_ID_RANGE": "30000-49000", + "L3_PARTITION_ID_RANGE": "50000-59000", + "LOOPBACK100_IP_RANGE": "10.10.0.0/24", + "MS_LOOPBACK_ID": "100", + "MS_UNDERLAY_AUTOCONFIG": "true", + "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", + "RP_SERVER_IP": "", + "TOR_AUTO_DEPLOY": "false", + "default_network": "Default_Network_Universal", + "default_vrf": "Default_VRF_Universal", + "enableScheduledBackup": "false", + "network_extension_template": "Default_Network_Extension_Universal", + "scheduledTime": "", + "vrf_extension_template": "Default_VRF_Extension_Universal" + }, + "provisionMode": "DCNMTopDown", + "replicationMode": "IngressReplication", + "templateName": "MSD_Fabric_11_1", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfTemplate": "Default_VRF_Universal" + }, + "mock_vrf12_object": { "ERROR": "", "RETURN_CODE": 200, "MESSAGE":"OK", diff --git a/tests/unit/modules/dcnm/test_dcnm_vrf.py b/tests/unit/modules/dcnm/test_dcnm_vrf.py index c7731438b..61ada10ce 100644 --- a/tests/unit/modules/dcnm/test_dcnm_vrf.py +++ b/tests/unit/modules/dcnm/test_dcnm_vrf.py @@ -40,6 +40,9 @@ class TestDcnmVrfModule(TestDcnmModule): mock_ip_sn = test_data.get("mock_ip_sn") vrf_inv_data = test_data.get("vrf_inv_data") fabric_details = test_data.get("fabric_details") + fabric_details_mfd = test_data.get("fabric_details_mfd") + fabric_details_vxlan = test_data.get("fabric_details_vxlan") + mock_vrf_attach_object_del_not_ready = test_data.get( "mock_vrf_attach_object_del_not_ready" ) @@ -61,6 +64,7 @@ def init_data(self): # Some of the mock data is re-initialized after each test as previous test might have altered portions # of the mock data. + self.mock_sn_fab_dict = copy.deepcopy(self.test_data.get("mock_sn_fab")) self.mock_vrf_object = copy.deepcopy(self.test_data.get("mock_vrf_object")) self.mock_vrf12_object = copy.deepcopy(self.test_data.get("mock_vrf12_object")) self.mock_vrf_attach_object = copy.deepcopy( @@ -119,6 +123,11 @@ def init_data(self): def setUp(self): super(TestDcnmVrfModule, self).setUp() + self.mock_dcnm_sn_fab = patch( + "ansible_collections.cisco.dcnm.plugins.modules.dcnm_vrf.get_sn_fabric_dict" + ) + self.run_dcnm_sn_fab = self.mock_dcnm_sn_fab.start() + self.mock_dcnm_ip_sn = patch( "ansible_collections.cisco.dcnm.plugins.modules.dcnm_vrf.get_fabric_inventory_details" ) @@ -179,6 +188,8 @@ def load_fixtures(self, response=None, device=""): ] elif "_merged_new" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_send.side_effect = [ self.blank_data, self.blank_data, @@ -188,6 +199,7 @@ def load_fixtures(self, response=None, device=""): elif "_merged_lite_new" in self._testMethodName: self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_send.side_effect = [ self.blank_data, self.blank_data, @@ -197,6 +209,8 @@ def load_fixtures(self, response=None, device=""): ] elif "error1" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_send.side_effect = [ self.blank_data, self.blank_data, @@ -205,6 +219,8 @@ def load_fixtures(self, response=None, device=""): ] elif "error2" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_send.side_effect = [ self.blank_data, self.blank_data, @@ -213,6 +229,8 @@ def load_fixtures(self, response=None, device=""): ] elif "error3" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_send.side_effect = [ self.blank_data, self.blank_data, @@ -250,6 +268,7 @@ def load_fixtures(self, response=None, device=""): elif "_merged_with_update" in self._testMethodName: self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object2] self.run_dcnm_send.side_effect = [ self.mock_vrf_object, @@ -262,6 +281,7 @@ def load_fixtures(self, response=None, device=""): elif "_merged_lite_update" in self._testMethodName: self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] self.run_dcnm_send.side_effect = [ self.mock_vrf_object, @@ -274,6 +294,7 @@ def load_fixtures(self, response=None, device=""): elif "_merged_lite_vlan_update" in self._testMethodName: self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] self.run_dcnm_send.side_effect = [ self.mock_vrf_object, @@ -287,6 +308,7 @@ def load_fixtures(self, response=None, device=""): elif "_merged_redeploy" in self._testMethodName: self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_send.side_effect = [ self.mock_vrf_object, self.mock_vrf_attach_object_pending, @@ -297,6 +319,7 @@ def load_fixtures(self, response=None, device=""): ] elif "_merged_lite_redeploy" in self._testMethodName: self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_send.side_effect = [ self.mock_vrf_object, self.mock_vrf_lite_obj, @@ -312,6 +335,7 @@ def load_fixtures(self, response=None, device=""): elif "replace_with_no_atch" in self._testMethodName: self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] self.run_dcnm_send.side_effect = [ self.mock_vrf_object, @@ -324,6 +348,7 @@ def load_fixtures(self, response=None, device=""): elif "replace_lite_no_atch" in self._testMethodName: self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object2] self.run_dcnm_send.side_effect = [ self.mock_vrf_object, @@ -336,6 +361,7 @@ def load_fixtures(self, response=None, device=""): elif "replace_with_changes" in self._testMethodName: self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] self.run_dcnm_send.side_effect = [ self.mock_vrf_object, @@ -348,6 +374,7 @@ def load_fixtures(self, response=None, device=""): elif "replace_lite_changes" in self._testMethodName: self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object2] self.run_dcnm_send.side_effect = [ self.mock_vrf_object, @@ -379,6 +406,7 @@ def load_fixtures(self, response=None, device=""): elif "lite_override_with_additions" in self._testMethodName: self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_send.side_effect = [ self.blank_data, self.blank_data, @@ -388,6 +416,7 @@ def load_fixtures(self, response=None, device=""): ] elif "override_with_additions" in self._testMethodName: + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_send.side_effect = [ self.blank_data, self.blank_data, @@ -397,6 +426,7 @@ def load_fixtures(self, response=None, device=""): elif "lite_override_with_deletions" in self._testMethodName: self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object2] self.run_dcnm_send.side_effect = [ self.mock_vrf_object, @@ -415,6 +445,7 @@ def load_fixtures(self, response=None, device=""): elif "override_with_deletions" in self._testMethodName: self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] self.run_dcnm_send.side_effect = [ self.mock_vrf_object, @@ -451,6 +482,7 @@ def load_fixtures(self, response=None, device=""): elif "delete_std" in self._testMethodName: self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] self.run_dcnm_send.side_effect = [ self.mock_vrf_object, @@ -466,6 +498,7 @@ def load_fixtures(self, response=None, device=""): elif "delete_std_lite" in self._testMethodName: self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object2] self.run_dcnm_send.side_effect = [ self.mock_vrf_object, @@ -480,6 +513,7 @@ def load_fixtures(self, response=None, device=""): elif "delete_failure" in self._testMethodName: self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] self.run_dcnm_send.side_effect = [ self.mock_vrf_object, @@ -499,6 +533,7 @@ def load_fixtures(self, response=None, device=""): obj1["DATA"][0].update({"vrfName": "test_vrf_dcnm"}) obj2["DATA"][0].update({"vrfName": "test_vrf_dcnm"}) + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object_dcnm_only] self.run_dcnm_send.side_effect = [ self.mock_vrf_object_dcnm_only, @@ -561,6 +596,9 @@ def load_fixtures(self, response=None, device=""): ] elif "_12merged_new" in self._testMethodName: + self.init_data() + # HERE + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_dict] self.run_dcnm_send.side_effect = [ self.blank_data, self.blank_data,