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/module_utils/brownfield_helper.py b/plugins/module_utils/brownfield_helper.py index 8a2cf18cdd..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) @@ -361,6 +362,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( diff --git a/plugins/modules/sda_fabric_devices_playbook_config_generator.py b/plugins/modules/sda_fabric_devices_playbook_config_generator.py index c3c4bb8665..1650df1153 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""" @@ -1120,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. @@ -1154,74 +1166,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.", @@ -1234,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", @@ -1258,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", @@ -1350,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", 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": {