Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Device Config backup workflow module #382

Merged
161 changes: 81 additions & 80 deletions plugins/modules/device_configs_backup_workflow_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from __future__ import absolute_import, division, print_function

__metaclass__ = type
__author__ = ("Abinash Mishra, Rugvedi Kapse, Madhan Sankaranarayanan")
__author__ = ("Abinash Mishra, Rugvedi Kapse, Madhan Sankaranarayanan, Sonali Deepthi Kesali")

DOCUMENTATION = r"""
---
Expand All @@ -21,6 +21,7 @@
author: Abinash Mishra (@abimishr)
Rugvedi Kapse (@rukapse)
Madhan Sankaranarayanan (@madhansansel)
Sonali Deepthi Kesali (@skesali)
options:
config_verify:
description: Set to True to verify the Cisco Catalyst Center config after applying the playbook config.
Expand Down Expand Up @@ -113,26 +114,29 @@
- one special characters from -=\\\\\\\\;,./~!@$%^&*()_+{}[]|:?"
type: str
requirements:
- dnacentersdk == 2.6.10
- dnacentersdk == 2.9.2
- python >= 3.5

notes:
- SDK Methods used are
sites.Sites.get_site
Site_design.Site_design.get_sites
sites.Sites.get_membership
site_design.Site_design.get_site_assigned_network_devices
devices.Devices.get_device_list
devices.Devices.get_device_by_id
configuration_archive.ConfigurationsArchive.export_device_configurations
file.Files.download_a_file_by_fileid
task.Task.get_task_by_id

- Paths used are
get /dna/intent/api/v1/site
get /dna/intent/api/v1/membership/${siteId}
get /dna/intent/api/v1/network-device
post /dna/intent/api/v1/network-device-archive/cleartext
get /dna/intent/api/v1/file/${fileId}
get /dna/intent/api/v1/task/${taskId}

get /dna/intent/api/v1/networkDevices/assignedToSite
get /dna/intent/api/v1/sites
get /dna/intent/api/v1/network-device/${id}
"""

EXAMPLES = r"""
Expand Down Expand Up @@ -394,6 +398,11 @@ class Device_configs_backup(DnacBase):
def __init__(self, module):
super().__init__(module)
self.skipped_devices_list = []
self.payload = module.params
self.keymap = {}
self.dnac_version = int(self.payload.get(
Copy link
Owner

Choose a reason for hiding this comment

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

setting dnac version is already done in dnac.py.. Can you check?

https://github.com/madhansansel/dnacenter-ansible/blob/main/plugins/module_utils/dnac.py
self.payload = module.params
self.dnac_version = int(self.payload.get("dnac_version").replace(".", ""))
# Dictionary to store multiple versions for easy maintenance and scalability
# To add a new version, simply update the 'dnac_versions' dictionary with the new version string as the key
# and the corresponding version number as the value.
self.dnac_versions = {
"2.3.5.3": 2353,
"2.3.7.6": 2376,
"2.2.3.3": 2233
# Add new versions here, e.g., "2.4.0.0": 2400
}

    # Dynamically create variables based on dictionary keys
    for version_key, version_value in self.dnac_versions.items():
        setattr(self, "version_" + version_key.replace(".", "_"), version_value)

Copy link
Author

Choose a reason for hiding this comment

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

Removed

"dnac_version").replace(".", ""))
self.version_2_3_5_3, self.version_2_3_7_6 = 2353, 2376

def validate_input(self):
"""
Expand Down Expand Up @@ -514,109 +523,101 @@ def validate_site_exists(self, site_name):
"""
site_exists = False
site_id = None
response = None

# Attempt to retrieve site information from Catalyst Center
try:
response = self.dnac._exec(
family="sites",
function="get_site",
op_modifies=True,
params={"name": site_name},
)
self.log("Response received post 'get_site' API call: {0}".format(str(response)), "DEBUG")
response = response.get("response")

# Process the response if available
if response:
site_id = response[0].get("id")
site_exists = True
else:
self.log("No response received from the 'get_site' API call.", "WARNING")
response = self.get_site(site_name)
Copy link
Owner

Choose a reason for hiding this comment

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

We don't need this API at all. Check get_site_id API and use it.. if you need site_exists also, then change the get_site_id API

Copy link
Author

Choose a reason for hiding this comment

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

Removed that API and directly using get_site_id

Copy link
Owner

Choose a reason for hiding this comment

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

We can directly use get_site_id rite?
Why do we use get_site first, and use get the site id from the response?

Copy link
Author

Choose a reason for hiding this comment

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

I removed and using get_site_id directly

Copy link
Owner

Choose a reason for hiding this comment

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

Can you update the code?

Copy link
Owner

Choose a reason for hiding this comment

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

We still don't need this API. What is the purpose of the API?

Copy link
Author

Choose a reason for hiding this comment

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

Updated

if response is None:
raise ValueError
site = response.get("response")
site_id = site[0].get("id")
site_exists = True

except Exception as e:
# Log an error message and fail if an exception occurs
self.log("An error occurred while retrieving site details for Site '{0}' using 'get_site' API call: {1}".format(site_name, str(e)), "ERROR")

# Update result if the site does not exist
if not site_exists:
self.msg = "An error occurred while retrieving site details for Site '{0}'. Please verify that the site exists.".format(site_name)
self.update_result("failed", False, self.msg, "ERROR")
self.status = "failed"
self.msg = ("An exception occurred: Site '{0}' does not exist in the Cisco Catalyst Center.".format(site_name))
self.result['response'] = self.msg
self.log(self.msg, "ERROR")
self.check_return_status()

return (site_exists, site_id)

def get_device_ids_from_site(self, site_name, site_id):
Copy link
Owner

Choose a reason for hiding this comment

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

Move this API to dnac.py and make it common....

Copy link
Author

Choose a reason for hiding this comment

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

Moved, I added two functions in dnac.py: one for getting device IDs from a site and another for retrieving the IP address from a device id.

"""
Retrieves device IDs from a specified site in Cisco Catalyst Center.
Retrieves the management IP addresses and their corresponding instance UUIDs of devices associated with a specific site in Cisco Catalyst Center.

Parameters:
site_name (str): The name of the site.
site_id (str): The ID of the site.
site_name (str): The name of the site whose devices' information is to be retrieved.
site_id (str): The unique identifier of the site.

Returns:
dict: A dictionary mapping management IP addresses to instance UUIDs of reachable devices that are not Unified APs.
dict: A dictionary mapping management IP addresses to their instance UUIDs.

Description:
This method queries Cisco Catalyst Center to retrieve device information associated with the provided site ID.
It filters out unreachable devices and Unified APs, returning a dictionary of management IP addresses mapped to their instance UUIDs.
Logs detailed information about the number of devices processed and skipped.
This method queries Cisco Catalyst Center to fetch the list of devices associated with the provided site.
It then extracts the management IP addresses and their instance UUIDs from the response.
Devices that are not reachable are logged as critical errors, and the function fails.
If no reachable devices are found for the specified site, it logs an error message and fails.
"""
mgmt_ip_to_instance_id_map = {}
processed_device_count = 0
skipped_device_count = 0

# Parameters for the site membership API call
site_params = {
"site_id": site_id,
}

# Attempt to retrieve device information associated with the site
try:
response = self.dnac._exec(
family="sites",
function="get_membership",
op_modifies=True,
params=site_params,
)
self.log("Response received post 'get_membership' API Call: {0} ".format(str(response)), "DEBUG")
if self.dnac_version <= self.version_2_3_5_3:
response = self.dnac._exec(
family="sites",
function="get_membership",
op_modifies=True,
params=site_params,
)
self.log("Response received post 'get_membership' API Call: {0}".format(str(response)), "DEBUG")

devices = response.get("device", [])
for item in devices:
for item_dict in item.get("response", []):
if item_dict["reachabilityStatus"] == "Reachable" and item_dict["family"] != "Unified AP":
mgmt_ip_to_instance_id_map[item_dict["managementIpAddress"]] = item_dict["instanceUuid"]
else:
msg = "Skipping device {0} in site {1} as its status is {2}".format(
item_dict["managementIpAddress"], site_name, item_dict.get("reachabilityStatus", "Unknown")
)
self.log(msg, "INFO" if item_dict["family"] == "Unified AP" else "WARNING")

# Process the response if available
if response:
response = response["device"]
# Iterate over the devices in the site membership
for item in response:
if item["response"]:
for item_dict in item["response"]:
processed_device_count += 1
# Check if the device is reachable
if item_dict["reachabilityStatus"] == "Reachable" and item_dict["collectionStatus"] == "Managed":
if item_dict["family"] != "Unified AP":
mgmt_ip_to_instance_id_map[item_dict["managementIpAddress"]] = item_dict["instanceUuid"]
else:
skipped_device_count += 1
self.skipped_devices_list.append(item_dict["managementIpAddress"])
msg = "Skipping device {0} in site {1} as its family is {2}".format(
item_dict["managementIpAddress"], site_name, item_dict["family"])
self.log(msg, "INFO")
else:
skipped_device_count += 1
self.skipped_devices_list.append(item_dict["managementIpAddress"])
msg = "Skipping device {0} in site {1} as its status is {2} or collectionStatus is {3} ".format(
item_dict["managementIpAddress"], site_name, item_dict["reachabilityStatus"], item_dict["collectionStatus"])
self.log(msg, "WARNING")
else:
# If unable to retrieve device information, log an error message
self.log("No response received from API call 'get_membership' to get membership information for site. {0}".format(site_name), "ERROR")
response = self.dnac._exec(
family="site_design",
function="get_site_assigned_network_devices",
op_modifies=True,
params=site_params,
)

# Log the total number of devices processed and skipped
self.log("Total number of devices processed: {0}".format(processed_device_count), "INFO")
self.log("Number of devices skipped due to being unreachable or APs: {0}".format(skipped_device_count), "INFO")
self.log("Filtered devices available for configuration backup: {0}".format(mgmt_ip_to_instance_id_map), "INFO")
self.log("Received API response from 'get_site_assigned_network_devices': {0}".format(str(response)), "DEBUG")
for device in response.get("response", []):
device_id = device.get("deviceId")
if device_id:
device_response = self.dnac._exec(
family="devices",
function="get_device_by_id",
op_modifies=True,
params={"id": device_id}
)

management_ip = device_response.get("response", {}).get("managementIpAddress")
if management_ip:
mgmt_ip_to_instance_id_map[management_ip] = device_id
else:
self.log(f"Management IP not found for device ID: {device_id}", "WARNING")

except Exception as e:
# Log an error message if any exception occurs during the process
self.log("Unable to fetch the device(s) associated to the site '{0}' due to {1}".format(site_name, str(e)), "ERROR")
self.log("Unable to fetch the device(s) associated with the site '{0}' due to {1}".format(site_name, str(e)), "ERROR")

# Log a warning if no reachable devices are found
if not mgmt_ip_to_instance_id_map:
self.log("Reachable devices not found at Site: {0}".format(site_name), "WARNING")
self.msg = "No reachable devices found at Site: {0}".format(site_name)
self.update_result("ok", False, self.msg, "INFO")
self.module.exit_json(**self.result)
Copy link
Owner

Choose a reason for hiding this comment

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

Why do we need to exit from the module if devices not found at site?

Copy link
Author

Choose a reason for hiding this comment

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

If the device ID is not available, then the IP address cannot be retrieved in this task


return mgmt_ip_to_instance_id_map

Expand Down