From a4fa3924694bf61b2968c99fa3703960cfcf882f Mon Sep 17 00:00:00 2001 From: Archit Soni Date: Fri, 10 Apr 2026 15:56:23 +0530 Subject: [PATCH 1/5] Making component specific filters as a list of dict instead of dict, as per the new design changes. UTs also updated --- ...ric_devices_playbook_config_generator.yaml | 16 +-- ...abric_devices_playbook_config_generator.py | 131 +++++++++--------- ...ric_devices_playbook_config_generator.json | 86 +++++++----- 3 files changed, 129 insertions(+), 104 deletions(-) diff --git a/playbooks/sda_fabric_devices_playbook_config_generator.yaml b/playbooks/sda_fabric_devices_playbook_config_generator.yaml index 6afc8bf784..de6ebf4853 100644 --- a/playbooks/sda_fabric_devices_playbook_config_generator.yaml +++ b/playbooks/sda_fabric_devices_playbook_config_generator.yaml @@ -94,7 +94,7 @@ component_specific_filters: components_list: ["fabric_devices"] fabric_devices: - fabric_name: "Global/USA/SAN-JOSE" + - fabric_name: "Global/USA/SAN-JOSE" # Example 4: Generate configuration for devices with specific roles in a fabric site - name: Generate configuration for border and control plane devices @@ -124,8 +124,8 @@ component_specific_filters: components_list: ["fabric_devices"] fabric_devices: - fabric_name: "Global/USA/SAN-JOSE" - device_roles: ["BORDER_NODE", "CONTROL_PLANE_NODE"] + - fabric_name: "Global/USA/SAN-JOSE" + device_roles: ["BORDER_NODE", "CONTROL_PLANE_NODE"] # Example 5: Generate configuration for a specific device in a fabric site - name: Generate configuration for a specific fabric device @@ -155,8 +155,8 @@ component_specific_filters: components_list: ["fabric_devices"] fabric_devices: - fabric_name: "Global/USA/SAN-JOSE" - device_ip: "10.0.0.1" + - fabric_name: "Global/USA/SAN-JOSE" + device_ip: "10.0.0.1" # Example 6: Auto-populate components_list from component filters - name: Generate configuration with auto-populated components_list @@ -187,7 +187,7 @@ # No components_list specified, but fabric_devices filters are provided # The 'fabric_devices' component will be automatically added to components_list fabric_devices: - fabric_name: "Global/USA/SAN-JOSE" + - fabric_name: "Global/USA/SAN-JOSE" # Example 7: Generate configuration with append mode - name: Generate and append SDA fabric device configuration @@ -217,5 +217,5 @@ component_specific_filters: components_list: ["fabric_devices"] fabric_devices: - fabric_name: "Global/India/Bangalore" - device_roles: ["BORDER_NODE"] + - fabric_name: "Global/India/Bangalore" + device_roles: ["BORDER_NODE"] diff --git a/plugins/modules/sda_fabric_devices_playbook_config_generator.py b/plugins/modules/sda_fabric_devices_playbook_config_generator.py index c3c4bb8665..af251439b5 100644 --- a/plugins/modules/sda_fabric_devices_playbook_config_generator.py +++ b/plugins/modules/sda_fabric_devices_playbook_config_generator.py @@ -87,7 +87,9 @@ - Filters specific to fabric device configuration retrieval. - Used to narrow down which fabric sites and devices should be included in the generated YAML file. - If no filters are provided, all fabric devices from all fabric sites in Cisco Catalyst Center will be retrieved. - type: dict + - Each list entry targets a specific fabric site and optionally narrows down by device IP or roles. + type: list + elements: dict suboptions: fabric_name: description: @@ -233,7 +235,7 @@ component_specific_filters: components_list: ["fabric_devices"] fabric_devices: - fabric_name: "Global/USA/SAN-JOSE" + - fabric_name: "Global/USA/SAN-JOSE" # Example 4: Generate configuration for devices with specific roles in a fabric site - name: Generate configuration for border and control plane devices @@ -263,8 +265,8 @@ component_specific_filters: components_list: ["fabric_devices"] fabric_devices: - fabric_name: "Global/USA/SAN-JOSE" - device_roles: ["BORDER_NODE", "CONTROL_PLANE_NODE"] + - fabric_name: "Global/USA/SAN-JOSE" + device_roles: ["BORDER_NODE", "CONTROL_PLANE_NODE"] # Example 5: Generate configuration for a specific device in a fabric site - name: Generate configuration for a specific fabric device @@ -294,8 +296,8 @@ component_specific_filters: components_list: ["fabric_devices"] fabric_devices: - fabric_name: "Global/USA/SAN-JOSE" - device_ip: "10.0.0.1" + - fabric_name: "Global/USA/SAN-JOSE" + device_ip: "10.0.0.1" # Example 6: Auto-populate components_list from component filters - name: Generate configuration with auto-populated components_list @@ -326,7 +328,7 @@ # No components_list specified, but fabric_devices filters are provided # The 'fabric_devices' component will be automatically added to components_list fabric_devices: - fabric_name: "Global/USA/SAN-JOSE" + - fabric_name: "Global/USA/SAN-JOSE" # Example 7: Generate configuration with append mode - name: Generate and append SDA fabric device configuration @@ -356,8 +358,8 @@ component_specific_filters: components_list: ["fabric_devices"] fabric_devices: - fabric_name: "Global/India/Bangalore" - device_roles: ["BORDER_NODE"] + - fabric_name: "Global/India/Bangalore" + device_roles: ["BORDER_NODE"] """ RETURN = r""" @@ -1154,74 +1156,79 @@ def get_fabric_devices_configuration(self, network_element, filters=None): if component_specific_filters: self.log( - "Processing component-specific filters", + f"Processing {len(component_specific_filters)} component-specific filter(s)", "DEBUG", ) - params_for_query = {} + for filter_idx, filter_entry in enumerate(component_specific_filters, 1): + self.log( + f"Processing filter entry {filter_idx}/{len(component_specific_filters)}: {self.pprint(filter_entry)}", + "DEBUG", + ) + params_for_query = {} + + fabric_name = filter_entry.get("fabric_name") + if fabric_name: + self.log(f"Applying fabric_name filter: '{fabric_name}'", "DEBUG") + fabric_site_id = self.fabric_site_name_to_id_dict.get(fabric_name) - fabric_name = component_specific_filters.get("fabric_name") - if fabric_name: - self.log(f"Applying fabric_name filter: '{fabric_name}'", "DEBUG") - fabric_site_id = self.fabric_site_name_to_id_dict.get(fabric_name) + if not fabric_site_id: + self.log( + f"Fabric site '{fabric_name}' not found in Cisco Catalyst Center. Skipping filter entry {filter_idx}.", + "WARNING", + ) + continue - if not fabric_site_id: self.log( - f"Fabric site '{fabric_name}' not found in Cisco Catalyst Center.", - "WARNING", + f"Fabric site '{fabric_name}' found with fabric_id '{fabric_site_id}'", + "DEBUG", ) - return {"fabric_devices": []} + params_for_query["fabric_id"] = fabric_site_id - self.log( - f"Fabric site '{fabric_name}' found with fabric_id '{fabric_site_id}'", - "DEBUG", - ) - params_for_query["fabric_id"] = fabric_site_id + device_ip = filter_entry.get("device_ip") + if device_ip: + self.log( + f"Applying device_ip filter: '{device_ip}'", + "DEBUG", + ) + device_list_params = self.get_device_list_params( + ip_address_list=device_ip + ) + device_info_map = self.get_device_list(device_list_params) + if not device_info_map or device_ip not in device_info_map: + self.log( + f"Device with IP '{device_ip}' not found in Cisco Catalyst Center. Skipping filter entry {filter_idx}.", + "WARNING", + ) + continue + + network_device_id = device_info_map[device_ip].get("device_id") + self.log( + f"Device with IP '{device_ip}' found with network_device_id '{network_device_id}'", + "DEBUG", + ) + self.log(f"Adding device_id filter: {network_device_id}", "DEBUG") + params_for_query["networkDeviceId"] = network_device_id - device_ip = component_specific_filters.get("device_ip") - if device_ip: - self.log( - f"Applying device_ip filter: '{device_ip}'", - "DEBUG", - ) - device_list_params = self.get_device_list_params( - ip_address_list=device_ip - ) - device_info_map = self.get_device_list(device_list_params) - if not device_info_map or device_ip not in device_info_map: + device_roles = filter_entry.get("device_roles") + if device_roles: self.log( - f"Device with IP '{device_ip}' not found in Cisco Catalyst Center.", - "WARNING", + f"Applying device_roles filter: {device_roles}", + "DEBUG", ) - return {"fabric_devices": []} + params_for_query["deviceRoles"] = device_roles - network_device_id = device_info_map[device_ip].get("device_id") - self.log( - f"Device with IP '{device_ip}' found with network_device_id '{network_device_id}'", - "DEBUG", - ) - self.log(f"Adding device_id filter: {network_device_id}", "DEBUG") - params_for_query["networkDeviceId"] = network_device_id + if not params_for_query: + self.log( + f"No valid filters provided for filter entry {filter_idx}, skipping.", + "WARNING", + ) + continue - device_roles = component_specific_filters.get("device_roles") - if device_roles: self.log( - f"Applying device_roles filter: {device_roles}", + f"Adding query parameters to list: {params_for_query}", "DEBUG", ) - params_for_query["deviceRoles"] = device_roles - - if not params_for_query: - self.log( - "No valid filters provided after processing component-specific filters.", - "WARNING", - ) - return {"fabric_devices": []} - - self.log( - f"Adding query parameters to list: {params_for_query}", - "DEBUG", - ) - fabric_devices_params_list_to_query.append(params_for_query) + fabric_devices_params_list_to_query.append(params_for_query) else: self.log( "No component-specific filters provided. Retrieving all fabric devices from all fabric sites.", diff --git a/tests/unit/modules/dnac/fixtures/sda_fabric_devices_playbook_config_generator.json b/tests/unit/modules/dnac/fixtures/sda_fabric_devices_playbook_config_generator.json index f0f43e73d0..860dfa1ef6 100644 --- a/tests/unit/modules/dnac/fixtures/sda_fabric_devices_playbook_config_generator.json +++ b/tests/unit/modules/dnac/fixtures/sda_fabric_devices_playbook_config_generator.json @@ -2,73 +2,91 @@ "generate_all_configurations_case_1": null, "filter_fabric_name_only_case_2": { "component_specific_filters": { - "fabric_devices": { - "fabric_name": "Global/Site_India/Karnataka/Bangalore" - } + "fabric_devices": [ + { + "fabric_name": "Global/Site_India/Karnataka/Bangalore" + } + ] } }, "filter_fabric_name_device_ip_case_3": { "component_specific_filters": { - "fabric_devices": { - "fabric_name": "Global/Site_India/Karnataka/Bangalore", - "device_ip": "205.1.1.10" - } + "fabric_devices": [ + { + "fabric_name": "Global/Site_India/Karnataka/Bangalore", + "device_ip": "205.1.1.10" + } + ] } }, "filter_fabric_name_edge_role_case_4": { "component_specific_filters": { - "fabric_devices": { - "fabric_name": "Global/Site_India/Karnataka/Bangalore", - "device_roles": ["EDGE_NODE"] - } + "fabric_devices": [ + { + "fabric_name": "Global/Site_India/Karnataka/Bangalore", + "device_roles": ["EDGE_NODE"] + } + ] } }, "filter_fabric_name_multi_roles_case_5": { "component_specific_filters": { - "fabric_devices": { - "fabric_name": "Global/Site_India/Karnataka/Bangalore", - "device_roles": ["BORDER_NODE", "CONTROL_PLANE_NODE"] - } + "fabric_devices": [ + { + "fabric_name": "Global/Site_India/Karnataka/Bangalore", + "device_roles": ["BORDER_NODE", "CONTROL_PLANE_NODE"] + } + ] } }, "filter_all_filters_case_6": { "component_specific_filters": { - "fabric_devices": { - "fabric_name": "Global/Site_India/Karnataka/Bangalore", - "device_ip": "205.1.1.10", - "device_roles": ["BORDER_NODE", "CONTROL_PLANE_NODE"] - } + "fabric_devices": [ + { + "fabric_name": "Global/Site_India/Karnataka/Bangalore", + "device_ip": "205.1.1.10", + "device_roles": ["BORDER_NODE", "CONTROL_PLANE_NODE"] + } + ] } }, "filter_fabric_name_cp_role_case_7": { "component_specific_filters": { - "fabric_devices": { - "fabric_name": "Global/India/Telangana/Hyderabad/BLD_1", - "device_roles": ["CONTROL_PLANE_NODE"] - } + "fabric_devices": [ + { + "fabric_name": "Global/India/Telangana/Hyderabad/BLD_1", + "device_roles": ["CONTROL_PLANE_NODE"] + } + ] } }, "filter_fabric_name_border_role_case_8": { "component_specific_filters": { - "fabric_devices": { - "fabric_name": "Global/Site_India/Karnataka/Bangalore", - "device_roles": ["BORDER_NODE"] - } + "fabric_devices": [ + { + "fabric_name": "Global/Site_India/Karnataka/Bangalore", + "device_roles": ["BORDER_NODE"] + } + ] } }, "filter_with_components_list_case_9": { "component_specific_filters": { "components_list": ["fabric_devices"], - "fabric_devices": { - "fabric_name": "Global/India/Telangana/Hyderabad/BLD_1" - } + "fabric_devices": [ + { + "fabric_name": "Global/India/Telangana/Hyderabad/BLD_1" + } + ] } }, "filter_with_file_mode_append_case_10": { "component_specific_filters": { - "fabric_devices": { - "fabric_name": "Global/Site_India/Karnataka/Bangalore" - } + "fabric_devices": [ + { + "fabric_name": "Global/Site_India/Karnataka/Bangalore" + } + ] } }, "get_sites_case_1": { From ffe0f2b25c0d3e34ef0cd0643ab346ba08067288 Mon Sep 17 00:00:00 2001 From: Archit Soni Date: Fri, 10 Apr 2026 16:13:34 +0530 Subject: [PATCH 2/5] Add validation for missing required filters in component-specific entries --- plugins/module_utils/brownfield_helper.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/module_utils/brownfield_helper.py b/plugins/module_utils/brownfield_helper.py index 8a2cf18cdd..3025734cda 100644 --- a/plugins/module_utils/brownfield_helper.py +++ b/plugins/module_utils/brownfield_helper.py @@ -361,6 +361,15 @@ def validate_component_specific_filters(self, component_specific_filters): ) continue + # Check for missing required filters in this entry + for req_filter_name, req_filter_spec in valid_filters_for_component.items(): + if req_filter_spec.get("required", False) and req_filter_name not in component_filter: + invalid_filters.append( + "Component '{0}' filter entry {1}/{2} is missing required filter '{3}'".format( + component_name, index, len(component_filters), req_filter_name + ) + ) + for filter_name, filter_value in component_filter.items(): self.log( "Processing filter '{0}' in entry {1}/{2} for component '{3}': value={4}".format( From 19412eec1dade9543adbdd04064d9c3b4bf8ab5b Mon Sep 17 00:00:00 2001 From: Archit Soni Date: Fri, 10 Apr 2026 16:49:46 +0530 Subject: [PATCH 3/5] Enhance fabric devices configuration retrieval with detailed parameter descriptions and improved handling of empty queries --- ...abric_devices_playbook_config_generator.py | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/plugins/modules/sda_fabric_devices_playbook_config_generator.py b/plugins/modules/sda_fabric_devices_playbook_config_generator.py index af251439b5..1650df1153 100644 --- a/plugins/modules/sda_fabric_devices_playbook_config_generator.py +++ b/plugins/modules/sda_fabric_devices_playbook_config_generator.py @@ -1122,15 +1122,25 @@ def retrieve_all_fabric_devices_from_api( def get_fabric_devices_configuration(self, network_element, filters=None): """ - Retrieve and transform fabric devices configuration. + Retrieve and transform fabric devices configuration into playbook-ready format. Parameters: - network_element (dict): Network element schema with API and transform details. - filters (dict, optional): Dictionary containing 'component_specific_filters'. - - component_specific_filters (list/dict): Filters for fabric_name, device_ip, device_roles. + network_element (dict): Network element schema containing: + - api_family (str): API family to use (e.g. 'sda'). + - api_function (str): API function name (e.g. 'get_fabric_devices'). + - reverse_mapping_function (callable): Returns the temp_spec OrderedDict for transformation. + filters (dict, optional): Dictionary containing: + - component_specific_filters (list of dict): Each entry may include: + - fabric_name (str): Name of the fabric site to filter by. + - device_ip (str): IP address of a specific device to filter by. + - device_roles (list of str): Roles to filter by (e.g. 'BORDER_NODE'). + If omitted or None, all fabric sites and their devices are retrieved. Returns: - dict: Dictionary with 'fabric_devices' key containing transformed device configs. + dict: Dictionary with key 'fabric_devices' mapping to a list of transformed fabric + site entries, each containing fabric_name and device_config list. + None: If no valid query parameters could be built from the provided filters, or if + no fabric devices are found matching the filters. Description: Main function to fetch fabric devices and transform them to playbook format. @@ -1241,6 +1251,12 @@ def get_fabric_devices_configuration(self, network_element, filters=None): ) fabric_devices_params_list_to_query.append({"fabric_id": fabric_id}) + if not fabric_devices_params_list_to_query: + self.log( + "No fabric devices parameters to query, Returning None" + ) + return None + self.log( f"Total fabric device queries to execute: {len(fabric_devices_params_list_to_query)}", "INFO", @@ -1265,10 +1281,10 @@ def get_fabric_devices_configuration(self, network_element, filters=None): if not all_fabric_devices: self.log( - "No fabric devices found matching the provided filters", + "No fabric devices found matching the provided filters, Returning None", "WARNING", ) - return {"fabric_devices": []} + return None self.log( f"Successfully retrieved {len(all_fabric_devices)} fabric device(s) for the provided filters", @@ -1357,6 +1373,11 @@ def get_fabric_devices_configuration(self, network_element, filters=None): transformed_fabric_devices_list = self.modify_parameters( temp_spec, fabric_entries_for_transformation ) + if not transformed_fabric_devices_list: + self.log( + "No fabric devices were transformed successfully, returning None", + ) + return None self.log( f"Transformation complete. Generated {len(transformed_fabric_devices_list)} fabric site(s) with devices", From ae333bb62e8f31391bbec26202f256c460401438 Mon Sep 17 00:00:00 2001 From: Archit Soni Date: Fri, 10 Apr 2026 17:26:27 +0530 Subject: [PATCH 4/5] Fix invalid components message to include valid components list in BrownFieldHelper --- plugins/module_utils/brownfield_helper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/brownfield_helper.py b/plugins/module_utils/brownfield_helper.py index 3025734cda..840459c3cd 100644 --- a/plugins/module_utils/brownfield_helper.py +++ b/plugins/module_utils/brownfield_helper.py @@ -290,8 +290,9 @@ def validate_component_specific_filters(self, component_specific_filters): comp for comp in components_list if comp not in network_elements ] if invalid_components: + valid_components = list(network_elements.keys()) + ["component_list"] self.msg = "Invalid network components provided for module '{0}': {1}. Valid components are: {2}".format( - self.module_name, invalid_components, list(network_elements.keys()) + self.module_name, invalid_components, valid_components ) self.fail_and_exit(self.msg) From 5c7582959d9106b65fb5320e6c3dec25731df920 Mon Sep 17 00:00:00 2001 From: Archit Soni Date: Tue, 14 Apr 2026 19:41:56 +0530 Subject: [PATCH 5/5] Addressing review comments --- plugins/module_utils/brownfield_helper.py | 2 +- ...abric_devices_playbook_config_generator.py | 35 +++++++++ ...ric_devices_playbook_config_generator.json | 14 ++++ ...abric_devices_playbook_config_generator.py | 74 +++++++++++++++++++ 4 files changed, 124 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/brownfield_helper.py b/plugins/module_utils/brownfield_helper.py index 840459c3cd..66726a5181 100644 --- a/plugins/module_utils/brownfield_helper.py +++ b/plugins/module_utils/brownfield_helper.py @@ -290,7 +290,7 @@ def validate_component_specific_filters(self, component_specific_filters): comp for comp in components_list if comp not in network_elements ] if invalid_components: - valid_components = list(network_elements.keys()) + ["component_list"] + valid_components = list(network_elements.keys()) + ["components_list"] self.msg = "Invalid network components provided for module '{0}': {1}. Valid components are: {2}".format( self.module_name, invalid_components, valid_components ) diff --git a/plugins/modules/sda_fabric_devices_playbook_config_generator.py b/plugins/modules/sda_fabric_devices_playbook_config_generator.py index 1650df1153..ccccd5e3ac 100644 --- a/plugins/modules/sda_fabric_devices_playbook_config_generator.py +++ b/plugins/modules/sda_fabric_devices_playbook_config_generator.py @@ -88,6 +88,10 @@ - Used to narrow down which fabric sites and devices should be included in the generated YAML file. - If no filters are provided, all fabric devices from all fabric sites in Cisco Catalyst Center will be retrieved. - Each list entry targets a specific fabric site and optionally narrows down by device IP or roles. + - Within a single entry, all specified filters are combined using AND + logic. Omitting a filter means no restriction on that attribute. + - Multiple entries are combined using OR logic, allowing retrieval from + different fabric sites in a single invocation. type: list elements: dict suboptions: @@ -360,6 +364,37 @@ fabric_devices: - fabric_name: "Global/India/Bangalore" device_roles: ["BORDER_NODE"] + +# Example 8: Generate configuration for devices from multiple fabric sites +- name: Generate configuration from multiple fabric sites + hosts: dnac_servers + vars_files: + - credentials.yml + gather_facts: false + connection: local + tasks: + - name: Export fabric devices from two fabric sites + cisco.dnac.sda_fabric_devices_playbook_config_generator: + dnac_host: "{{ dnac_host }}" + dnac_port: "{{ dnac_port }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_debug: "{{ dnac_debug }}" + dnac_version: "{{ dnac_version }}" + dnac_log: true + dnac_log_level: DEBUG + dnac_log_append: false + dnac_log_file_path: "{{ dnac_log_file_path }}" + state: gathered + config: + component_specific_filters: + components_list: ["fabric_devices"] + fabric_devices: + - fabric_name: "Global/USA/SAN-JOSE" + device_roles: ["BORDER_NODE"] + - fabric_name: "Global/India/Bangalore" + device_roles: ["EDGE_NODE"] """ RETURN = r""" diff --git a/tests/unit/modules/dnac/fixtures/sda_fabric_devices_playbook_config_generator.json b/tests/unit/modules/dnac/fixtures/sda_fabric_devices_playbook_config_generator.json index 860dfa1ef6..1081c6e334 100644 --- a/tests/unit/modules/dnac/fixtures/sda_fabric_devices_playbook_config_generator.json +++ b/tests/unit/modules/dnac/fixtures/sda_fabric_devices_playbook_config_generator.json @@ -455,6 +455,20 @@ ], "version": "1.0" }, + "filter_multi_fabric_sites_case_11": { + "component_specific_filters": { + "fabric_devices": [ + { + "fabric_name": "Global/Site_India/Karnataka/Bangalore", + "device_roles": ["EDGE_NODE"] + }, + { + "fabric_name": "Global/India/Telangana/Hyderabad/BLD_1", + "device_roles": ["CONTROL_PLANE_NODE"] + } + ] + } + }, "get_fabric_devices_filtered_cp_node_fabric_3_case_1": { "response": [ { diff --git a/tests/unit/modules/dnac/test_sda_fabric_devices_playbook_config_generator.py b/tests/unit/modules/dnac/test_sda_fabric_devices_playbook_config_generator.py index 16122852ea..5b11289ec3 100644 --- a/tests/unit/modules/dnac/test_sda_fabric_devices_playbook_config_generator.py +++ b/tests/unit/modules/dnac/test_sda_fabric_devices_playbook_config_generator.py @@ -71,6 +71,9 @@ class TestDnacBrownfieldSdaFabricDevicesPlaybookGenerator(TestDnacModule): playbook_config_filter_with_file_mode_append_case_10 = test_data.get( "filter_with_file_mode_append_case_10" ) + playbook_config_filter_multi_fabric_sites_case_11 = test_data.get( + "filter_multi_fabric_sites_case_11" + ) def setUp(self): super(TestDnacBrownfieldSdaFabricDevicesPlaybookGenerator, self).setUp() @@ -407,6 +410,50 @@ def load_fixtures(self, response=None, device=""): self.test_data.get("get_layer3_sda_transit_handoffs_empty_response"), ] + elif "test_filter_multi_fabric_sites_case_11" in self._testMethodName: + # Test Case 11: Filter across multiple fabric sites with different role filters + # - Bangalore: EDGE_NODE (2 devices) + # - Hyderabad: CONTROL_PLANE_NODE (1 device) + + self.run_dnac_exec.side_effect = [ + # Initialization phase + self.test_data.get("get_sites_case_1"), + self.test_data.get("get_fabric_sites_case_1"), + self.test_data.get("get_transit_networks_case_1"), + # get_fabric_devices for Bangalore with EDGE_NODE filter - returns 2 edge nodes + self.test_data.get("get_fabric_devices_filtered_edge_nodes_case_1"), + # get_network_device_list for 2 edge node devices + self.test_data.get("get_device_list_device_1_case_1"), # 205.1.2.67 + self.test_data.get("get_device_list_device_3_case_1"), # 205.1.2.68 + # get_fabric_site_wired_settings for Bangalore + self.test_data.get( + "get_embedded_wireless_controller_settings_empty_response" + ), + # Border handoff settings for 2 Bangalore edge node devices + # Device 1 + self.test_data.get("get_layer2_handoffs_empty_response"), + self.test_data.get("get_layer3_ip_transit_handoffs_empty_response"), + self.test_data.get("get_layer3_sda_transit_handoffs_empty_response"), + # Device 3 + self.test_data.get("get_layer2_handoffs_empty_response"), + self.test_data.get("get_layer3_ip_transit_handoffs_empty_response"), + self.test_data.get("get_layer3_sda_transit_handoffs_empty_response"), + # get_fabric_devices for Hyderabad with CONTROL_PLANE_NODE filter - returns 1 device + self.test_data.get( + "get_fabric_devices_filtered_cp_node_fabric_3_case_1" + ), + # get_network_device_list for 1 CP device + self.test_data.get("get_device_list_device_5_case_1"), # 172.27.248.222 + # get_fabric_site_wired_settings for Hyderabad + self.test_data.get( + "get_embedded_wireless_controller_settings_empty_response" + ), + # Border handoff settings for 1 Hyderabad CP node device + self.test_data.get("get_layer2_handoffs_empty_response"), + self.test_data.get("get_layer3_ip_transit_handoffs_empty_response"), + self.test_data.get("get_layer3_sda_transit_handoffs_empty_response"), + ] + elif "test_filter_with_file_mode_append_case_10" in self._testMethodName: # Test Case 10: Test file_mode append functionality # This tests the append mode parameter (same as case 2 but with append mode) @@ -737,6 +784,33 @@ def test_filter_with_components_list_case_9(self): str(result.get("msg")), ) + def test_filter_multi_fabric_sites_case_11(self): + """ + Test Case 11: Filter across multiple fabric sites with different role filters. + - Bangalore: EDGE_NODE (2 devices) + - Hyderabad/BLD_1: CONTROL_PLANE_NODE (1 device) + Expected: Returns 3 fabric devices total across 2 fabric sites. + """ + set_module_args( + dict( + dnac_host="1.1.1.1", + dnac_username="dummy", + dnac_password="dummy", + dnac_version="2.3.7.9", + dnac_log=True, + state="gathered", + dnac_log_level="DEBUG", + file_path="/tmp/ut_case11_multi_fabric_sites.yaml", + config=self.playbook_config_filter_multi_fabric_sites_case_11, + ) + ) + + result = self.execute_module(changed=True, failed=False) + self.assertIn( + "YAML configuration file generated successfully for module 'sda_fabric_devices_workflow_manager'", + str(result.get("msg")), + ) + def test_filter_with_file_mode_append_case_10(self): """ Test Case 10: Test file_mode append functionality.