Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .coverage
Binary file not shown.
161 changes: 142 additions & 19 deletions plugins/modules/fabric_devices_info_workflow_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@
choices:
- fabric_info # Fabric configuration details, device roles, and fabric site associations
- handoff_info # Layer 2/3 handoff configurations for border and control plane nodes
- onboarding_info # Device provisioning status, port assignments, and SSID details for wireless devices
- onboarding_info # Device provisioning status, port assignments, port channels and SSID details for wireless devices
- connected_devices_info # Neighbor device information via CDP/LLDP discovery protocols
- device_health_info # Health metrics including CPU, memory, temperature, and performance data
- device_issues_info # Active alerts, issues, and problems detected on fabric devices
Expand Down Expand Up @@ -1882,12 +1882,18 @@ def get_diff_merged(self, config):
combined_fabric_data["device_health_info"] = device_health_result

if onboarding_info:
self.log("Retrieving device onboarding status and provisioning details for {0} fabric devices".format(len(fabric_devices)), "DEBUG")
self.log("Retrieving device onboarding and provisioning status information", "DEBUG")
self.log("Retrieving device onboarding status and port assignment details for {0} fabric devices".format(len(fabric_devices)), "DEBUG")
self.log("Retrieving device onboarding and port assignment information", "DEBUG")
onboarding_info_result = self.get_port_details(filtered_fabric_devices)
self.total_response.append(onboarding_info_result)
combined_fabric_data["onboarding_info"] = onboarding_info_result

self.log("Retrieving device onboarding status and port channel details for {0} fabric devices".format(len(fabric_devices)), "DEBUG")
self.log("Retrieving device onboarding and port channel information", "DEBUG")
port_channel_info_result = self.get_port_channels(filtered_fabric_devices)
self.total_response.append(port_channel_info_result)
combined_fabric_data["port_channel_info"] = port_channel_info_result

self.log("Retrieving SSID configuration details for wireless fabric devices", "DEBUG")
ssid_info_result = self.get_ssid_details(filtered_fabric_devices)
self.total_response.append(ssid_info_result)
Expand Down Expand Up @@ -2882,18 +2888,18 @@ def get_handoff_layer3_ip_info(self, filtered_fabric_devices):
for handoff in layer3_ip_handoff_data:
transit_id = handoff.get("transitNetworkId")
handoff["transitName"] = self.get_transit_name_by_id(transit_id)
devices_with_handoffs += 1
self.log(
"Layer 3 IP handoff configuration found for fabric device {0} - "
"retrieved {1} handoff records".format(
ip, len(layer3_ip_handoff_data)
),
"INFO"
)
all_handoff_layer3_ip_info_list.append({
"device_ip": ip,
"handoff_layer3_ip_transit_info": layer3_ip_handoff_data
})
devices_with_handoffs += 1
self.log(
"Layer 3 IP handoff configuration found for fabric device {0} - "
"retrieved {1} handoff records".format(
ip, len(layer3_ip_handoff_data)
),
"INFO"
)
all_handoff_layer3_ip_info_list.append({
"device_ip": ip,
"handoff_layer3_ip_transit_info": layer3_ip_handoff_data
})
else:
devices_without_handoffs += 1
self.log(
Expand Down Expand Up @@ -3885,14 +3891,14 @@ def get_port_details(self, filtered_fabric_devices):
self.log("Onboarding data found for device IP: {0}".format(ip), "INFO")
all_onboarding_info_list.append({
"device_ip": ip,
"port_details": onboarding_data
"port_assignment_details": onboarding_data
})
else:
devices_without_onboarding_data += 1
self.log("No onboarding data found for device IP: {0}".format(ip), "DEBUG")
all_onboarding_info_list.append({
"device_ip": ip,
"port_details": []
"port_assignment_details": []
})
continue

Expand All @@ -3901,10 +3907,10 @@ def get_port_details(self, filtered_fabric_devices):
self.msg = "Exception occurred while getting port assignment details for device {0}: {1}".format(ip, api_err)
all_onboarding_info_list.append({
"device_ip": ip,
"port_details": "Error: {0}".format(api_err)
"port_assignment_details": "Error: {0}".format(api_err)
})

result = [{"device_onboarding_info": all_onboarding_info_list}]
result = [{"port_assignment_info": all_onboarding_info_list}]

total_fabric_devices = len(filtered_fabric_devices)
self.log(
Expand All @@ -3930,6 +3936,123 @@ def get_port_details(self, filtered_fabric_devices):

return result

def get_port_channels(self, filtered_fabric_devices):
"""
Retrieve SDA port channel configurations for fabric device interface aggregation and redundancy analysis.

This method queries the Catalyst Center SDA API to collect port channel details for fabric
devices, providing insights into interface aggregation configurations, VLAN assignments, and
connected device information essential for fabric network redundancy and bandwidth management.

Args:
filtered_fabric_devices (dict): Mapping of device management IP addresses to their fabric IDs.
Contains only devices that have been confirmed as members of the specified fabric site.

Returns:
list: A list with a single dictionary containing port channel information:
[
{
"device_onboarding_info": [
{
"device_ip": "192.168.1.1",
"port_channel_details": [port_channel_records] or [] or "Error: <error_message>"
}
]
}
]

"""
self.log("Retrieving fabric device onboarding information for lifecycle management and troubleshooting", "INFO")
self.log("Processing port channel details for {0} fabric devices across fabric sites".format(len(filtered_fabric_devices)), "DEBUG")

device_identifier = self.want["fabric_devices"][0].get("device_identifier")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.log(
    "Port channel retrieval configuration - device_identifier specified: {0}".format(
        bool(device_identifier)
    ),
    "DEBUG"
)

all_port_channel_info_list = []

# Statistics tracking for port channel operations
statistics = {
    'devices_processed': 0,
    'devices_with_port_channels': 0,
    'devices_without_port_channels': 0,
    'devices_with_errors': 0,
    'total_port_channels_retrieved': 0,
    'total_api_calls': 0
}

self.log(
    "Beginning port channel data collection across {0} fabric devices".format(
        len(filtered_fabric_devices)
    ),
    "INFO"
)


all_onboarding_info_list = []
devices_processed = 0
devices_with_port_channels_data = 0
devices_without_port_channels_data = 0
devices_with_errors = 0

for index, (ip, fabric_id) in enumerate(filtered_fabric_devices.items()):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    self.log(
        "Processing outer loop for device {0}/{1} - "
        "IP: {2}, Fabric ID: {3}".format(
            index, len(filtered_fabric_devices), device_ip, fabric_id
        ),
        "DEBUG"
    )

ip_device_uuid_map = self.get_device_ids_from_device_ips([ip])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    if not ip_device_uuid_map or device_ip not in ip_device_uuid_map:
        self.log(
            "Failed to retrieve device UUID for IP {0} - skipping port channel retrieval".format(
                device_ip
            ),
            "WARNING"
        )
        statistics['devices_with_errors'] += 1
        all_port_channel_info_list.append({
            "device_ip": device_ip,
            "port_channel_details": "Error: Unable to retrieve device UUID"
        })
        continue
    
    # Inner loop processing each device IP and UUID mapping
    for ip, device_uuid in ip_device_uuid_map.items():

for ip, device_uuid in ip_device_uuid_map.items():
devices_processed += 1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        statistics['devices_processed'] += 1
        
        self.log(
            "Processing inner loop for device {0} - UUID: {1}".format(
                ip, device_uuid
            ),
            "DEBUG"
        )
        
        self.log(
            "Initiating port channel data retrieval for fabric device {0}".format(ip),
            "DEBUG"
        )

self.log(
"Processing onboarding device detail for device {0}/{1}: "
"IP: {2}".format(index + 1, len(filtered_fabric_devices), ip),
"DEBUG"
)
try:
params = {"fabric_id": fabric_id}

if device_identifier or fabric_id:
params["network_device_id"] = device_uuid
self.log(
"Added 'network_device_id' parameter for device {0}: {1}".format(ip, device_uuid),
"DEBUG"
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

            statistics['total_api_calls'] += 1

response = self.dnac._exec(
family="sda",
function="get_port_channels",
params=params
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

            # Validate API response structure
            if not response or not isinstance(response, dict):
                self.log(
                    "Invalid API response structure for device {0} - "
                    "expected dict, got: {1}".format(
                        ip, type(response).__name__
                    ),
                    "WARNING"
                )
                statistics['devices_with_errors'] += 1
                all_port_channel_info_list.append({
                    "device_ip": ip,
                    "port_channel_details": "Error: Invalid API response structure"
                })
                continue
            
            port_channel_data = response.get("response", [])
            
            if not isinstance(port_channel_data, list):
                self.log(
                    "Unexpected response data type for device {0} - "
                    "expected list, got: {1}".format(
                        ip, type(port_channel_data).__name__
                    ),
                    "WARNING"
                )
                statistics['devices_with_errors'] += 1
                all_port_channel_info_list.append({
                    "device_ip": ip,
                    "port_channel_details": "Error: Unexpected response data format"
                })
                continue

onboarding_data = response.get("response", [])
self.log(
"Received API response from 'get_port_channels' for device {0}: {1}".format(
ip, response
),
"DEBUG"
)
if onboarding_data:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

           if port_channel_data:
                statistics['devices_with_port_channels'] += 1
                statistics['total_port_channels_retrieved'] += len(port_channel_data)
                
                self.log(
                    "Port channel configuration found for fabric device {0} - "
                    "retrieved {1} port channel records".format(
                        ip, len(port_channel_data)
                    ),
                    "INFO"
                )
                
                all_port_channel_info_list.append({
                    "device_ip": ip,
                    "port_channel_details": port_channel_data
                })
                
                for idx, port_channel in enumerate(port_channel_data, start=1):
                    port_channel_id = port_channel.get("id", "Unknown")
                    interface_name = port_channel.get("interfaceName", "Unknown")
                    
                    self.log(
                        "Port channel {0}/{1} details for device {2} - "
                        "ID: {3}, Interface: {4}".format(
                            idx, len(port_channel_data), ip, 
                            port_channel_id, interface_name
                        ),
                        "DEBUG"
                    )
                
            else:
                statistics['devices_without_port_channels'] += 1
                
                self.log(
                    "No port channel configuration found for fabric device {0} - "
                    "device may not have configured port channels".format(ip),
                    "DEBUG"
                )
                
                all_port_channel_info_list.append({
                    "device_ip": ip,
                    "port_channel_details": []
                })

devices_with_port_channels_data += 1
self.log("Port channel data found for device IP: {0}".format(ip), "INFO")
all_onboarding_info_list.append({
"device_ip": ip,
"port_channel_details": onboarding_data
})
else:
devices_without_port_channels_data += 1
self.log("No port channel data found for device IP: {0}".format(ip), "DEBUG")
all_onboarding_info_list.append({
"device_ip": ip,
"port_channel_details": []
})
continue

except Exception as api_err:
devices_with_errors += 1
self.msg = "Exception occurred while getting port assignment details for device {0}: {1}".format(ip, api_err)
all_onboarding_info_list.append({
"device_ip": ip,
"port_channel_details": "Error: {0}".format(api_err)
})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

continue


result = [{"port_channel_info": all_onboarding_info_list}]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

result = [{"port_channel_info": all_port_channel_info_list}]

self.log(
    "Port channel configuration retrieval completed - "
    "devices processed: {0}, with port channels: {1}, "
    "without port channels: {2}, with errors: {3}".format(
        statistics['devices_processed'],
        statistics['devices_with_port_channels'],
        statistics['devices_without_port_channels'],
        statistics['devices_with_errors']
    ),
    "INFO"
)

self.log(
    "Port channel retrieval statistics - "
    "total API calls: {0}, total port channels retrieved: {1}".format(
        statistics['total_api_calls'],
        statistics['total_port_channels_retrieved']
    ),
    "INFO"
)

if statistics['devices_with_port_channels'] > 0:
    self.log(
        "Fabric devices with port channel configurations indicating "
        "successful interface aggregation: {0}".format(
            statistics['devices_with_port_channels']
        ),
        "INFO"
    )

if statistics['devices_without_port_channels'] > 0:
    self.log(
        "Fabric devices without port channel configurations: {0}".format(
            statistics['devices_without_port_channels']
        ),
        "INFO"
    )

if statistics['devices_with_errors'] > 0:
    self.log(
        "Warning: {0} devices encountered errors during port channel "
        "configuration retrieval - check individual device logs for details".format(
            statistics['devices_with_errors']
        ),
        "WARNING"
    )

successful_devices = [
    entry["device_ip"] for entry in all_port_channel_info_list 
    if isinstance(entry["port_channel_details"], list) and entry["port_channel_details"]
]

if successful_devices:
    self.log(
        "Successfully retrieved port channel configurations for devices: {0}".format(
            successful_devices
        ),
        "DEBUG"
    )

self.log(
    "Port channel configuration retrieval operation completed for {0} "
    "fabric devices with {1} total device entries processed".format(
        len(filtered_fabric_devices), len(all_port_channel_info_list)
    ),
    "INFO"
)

self.log(
    "Final aggregated port channel information result: {0}".format(result),
    "DEBUG"
)

return result


total_fabric_devices = len(filtered_fabric_devices)
self.log(
"Fabric device onboarding information retrieval completed - "
"processed {0}/{1} fabric devices successfully".format(
devices_processed,
total_fabric_devices
),
"INFO"
)

if devices_with_port_channels_data > 0:
self.log("Fabric devices with port channel data indicating successful fabric integration: {0}".format(devices_with_port_channels_data), "INFO")

if devices_without_port_channels_data > 0:
self.log("Fabric devices without port channel data indicating onboarding issues: {0}".format(devices_without_port_channels_data), "INFO")

if devices_with_errors > 0:
self.log("Warning: {0} devices encountered errors during onboarding information retrieval".format(devices_with_errors), "WARNING")

self.log("Completed onboarding info retrieval. Total devices processed: {0}".format(len(all_onboarding_info_list)), "INFO")
self.log("Aggregated device-onboarding info: {0}".format(result), "DEBUG")

return result

def write_device_info_to_file(self, filtered_config):
"""
Write collected fabric device information to a specified file with comprehensive format support and error handling.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1092,6 +1092,8 @@
],
"version": "1.0"
},
"192.168.200.69_4": {"response": [{"description": "Cisco IOS Software [Bengaluru], Catalyst L3 Switch Software (CAT9K_IOSXE), Version 17.6.6, RELEASE SOFTWARE (fc3) Technical Support: http://www.cisco.com/techsupport Copyright (c) 1986-2023 by Cisco Systems, Inc. Compiled Wed 27-Sep-23 19:11 by mcpre netconf enabled", "deviceSupportLevel": "Supported", "softwareType": "IOS-XE", "softwareVersion": "17.6.6", "lastUpdateTime": 1762320447587, "macAddress": "dc:77:4c:d3:d5:80", "serialNumber": "FOC2443L1VQ", "pendingSyncRequestsCount": "0", "reasonsForDeviceResync": "Config Change Event", "reasonsForPendingSyncRequests": "", "inventoryStatusDetail": "<status><general code=SUCCESS/></status>", "syncRequestedByApp": "", "collectionInterval": "Global Default", "dnsResolvedManagementAddress": "192.168.200.69", "lastManagedResyncReasons": "Config Change Event", "managementState": "Managed", "upTime": "1:31:19.46", "roleSource": "MANUAL", "interfaceCount": "0", "associatedWlcIp": "", "lastUpdated": "2025-11-05 05:27:27", "bootDateTime": "2025-11-05 03:56:27", "apManagerInterfaceIp": "", "collectionStatus": "Managed", "family": "Switches and Hubs", "hostname": "Fabric-9300-2-2.rcdnlabcead.com", "locationName": null, "managementIpAddress": "192.168.200.69", "platformId": "C9300-48U", "reachabilityFailureReason": "", "reachabilityStatus": "Reachable", "series": "Cisco Catalyst 9300 Series Switches", "snmpContact": "", "snmpLocation": "", "apEthernetMacAddress": null, "errorCode": null, "errorDescription": null, "lastDeviceResyncStartTime": "2025-11-05 05:27:23", "lineCardCount": "0", "lineCardId": "", "managedAtleastOnce": true, "memorySize": "NA", "tagCount": "0", "tunnelUdpPort": null, "uptimeSeconds": 7471, "vendor": "Cisco", "waasDeviceMode": null, "type": "Cisco Catalyst 9300 Switch", "location": null, "role": "ACCESS", "instanceUuid": "199950bc-c0d6-42f6-b1f3-a3f5aed176ee", "instanceTenantId": "646e980e47fc23431d046907", "id": "199950bc-c0d6-42f6-b1f3-a3f5aed176ee"}], "version": "1.0"},
"get_port_channels": {"response": [], "version": "1.0"},
"get_network_device_by_ip": {
"response": {
"description": "Cisco IOS Software [Dublin], Catalyst L3 Switch Software (CAT9K_IOSXE), Version 17.12.4, RELEASE SOFTWARE (fc3) Technical Support: http://www.cisco.com/techsupport Copyright (c) 1986-2024 by Cisco Systems, Inc. Compiled Tue 23-Jul-24 09:40 by mcpre netconf enabled",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ def load_fixtures(self, response=None, device=""):
self.test_data.get("get_fabric_devices11"),
self.test_data.get("192.168.200.69_10"),
self.test_data.get("get_port_assignments"),
self.test_data.get("192.168.200.69_4"),
self.test_data.get("get_port_channels"),
self.test_data.get("get_network_device_by_ip"),
self.test_data.get("get_provisioned_wired_device")
]
Expand Down Expand Up @@ -423,10 +425,10 @@ def test_fabric_devices_info_workflow_manager_playbook_onboarding_info(self):
"The fabric devices filtered from the network devices are: ['192.168.200.69']",
[
{
"device_onboarding_info": [
"port_assignment_info": [
{
"device_ip": "192.168.200.69",
"port_details": [
"port_assignment_details": [
{
"authenticateTemplateName": "No Authentication",
"connectedDeviceType": "ACCESS_POINT",
Expand Down Expand Up @@ -462,6 +464,16 @@ def test_fabric_devices_info_workflow_manager_playbook_onboarding_info(self):
]
}
],
[
{
"port_channel_info": [
{
"device_ip": "192.168.200.69",
"port_channel_details": []
}
]
}
],
[
{
"ssid_info": [
Expand Down
Loading