From 01e1effb954725ffe8d7664909451742ff7aa72c Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 5 Mar 2024 16:24:31 +0530 Subject: [PATCH 01/17] Write the API for the creation/updation of tag for devices/interfaces with or without dynamic rules, assigning device/interface members to the tag, removing device/interface members from the tag, deleting the tag from Catalyst center, added documentation and examples for the same. --- playbooks/inventory_workflow_manager.yml | 40 + plugins/modules/inventory_workflow_manager.py | 1212 ++++++++++++++++- 2 files changed, 1193 insertions(+), 59 deletions(-) diff --git a/playbooks/inventory_workflow_manager.yml b/playbooks/inventory_workflow_manager.yml index 6a54a24df3..11f58ce939 100644 --- a/playbooks/inventory_workflow_manager.yml +++ b/playbooks/inventory_workflow_manager.yml @@ -40,6 +40,46 @@ type: "{{item.type}}" device_resync: "{{item.device_resync}}" reboot_device: "{{item.reboot_device}}" + create_tag: + tag_name: Pre_test_check1 + systemTag: False + description: "Add description while creating the tag" + dynamic_rules: + - member_type: "networkdevice" + rules: + operation: "AND" + items: + - operation: "OR" + items: + - operation: "ILIKE" + name: "hostname" + value: "%switch%" + - operation: "ILIKE" + name: "hostname" + value: "%Router%" + - operation: ILIKE + name: family + value: "%Switch%" + + update_tag: + tag_name: Pre_test_check1 + system_tag: False + description: "Add description while updating the tag" + + assign_device_member_to_tag: + tag_name: Pre_test_check1 + assign_interface_member_to_tag: + interface_names_list: ['GigabitEthernet1/0/1','GigabitEthernet1/0/3'] + tag_name: Pre_test_check1 + + remove_device_tag_member: + tag_name: March_4th + remove_interface_tag_member: + interface_names_list: ['GigabitEthernet1/0/1','GigabitEthernet1/0/3'] + tag_name: March_4th + delete_tag: + tag_name: string_device_tag + update_device_role: role: "{{item.role}}" add_user_defined_field: diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index 01f12dc9b4..15ca9cf47d 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -350,6 +350,135 @@ process. If unspecified, the system will check the device status every 2 seconds by default. type: int default: 2 + create_tag: + description: This parameter used to collect all the details needed to create the tag. + type: dict + suboptions: + system_tag: + description: It's a unique identifier associated with each tag in Cisco Catalyst Center, used internally by the system + for management and reference purposes. Specifies whether the tag is a system tag or not. + type: bool + description: + description: Provides a description for the tag, explaining its purpose or usage. + type: str + dynamic_rules: + description: Defines dynamic rules associated with the tag, which specify conditions for membership. + type: list + elements: dict + suboptions: + member_type: + description: Specifies the type of member (networkdevice, interface) targeted by the dynamic rule. + - 'networkdevice' - Indicates that the dynamic rule applies to network devices. + - 'interface' - Indicates that the dynamic rule applies to interfaces. + type: str + rules: + description: Contains the conditions/rules for membership. + type: dict + suboptions: + items: + description: Represents the items for membership. + type: list + elements: dict + suboptions: + operation: + description: Specifies the operation used to evaluate the condition (equals, not equals, etc.). + type: str + name: + description: Specifies the name of the attribute to evaluate for membership. + type: str + value: + description: Specifies the value to compare with the attribute for membership. + type: str + tag_name: + description: Specifies the name of the tag. It's required while creating the tag. + type: str + update_tag: + description: This parameter used to collect all the details needed to update the tag. + type: dict + suboptions: + system_tag: + description: It's a unique identifier associated with each tag in Cisco Catalyst Center, used internally by the system + for management and reference purposes. Specifies whether the tag is a system tag or not. + type: bool + description: + description: Provides a description for the tag, explaining its purpose or usage. + type: str + dynamic_rules: + description: Defines dynamic rules associated with the tag, which specify conditions for membership. + type: list + elements: dict + suboptions: + member_type: + description: Specifies the type of member (networkdevice, interface) targeted by the dynamic rule. + - 'networkdevice' - Indicates that the dynamic rule applies to network devices. + - 'interface' - Indicates that the dynamic rule applies to interfaces. + type: str + rules: + description: Contains the conditions/rules for membership. + type: dict + suboptions: + items: + description: Represents the items for membership. + type: list + elements: dict + suboptions: + operation: + description: Specifies the operation used to evaluate the condition (equals, not equals, etc.). + type: str + name: + description: Specifies the name of the attribute to evaluate for membership. + type: str + value: + description: Specifies the value to compare with the attribute for membership. + type: str + name: + description: Specifies the name of the tag. It's required while creating the tag. + type: str + assign_device_member_to_tag: + description: Assigning the device members to the tag. + type: dict + suboptions: + tag_name: + description: Name of the tag which is assigned to the device members. + type: str + assign_interface_member_to_tag: + description: Assigning the interface members to the tag. + type: dict + suboptions: + tag_name: + description: Name of the tag which is assigned to the device members + type: str + interface_names_list: + description: Specify the list of interface names to to which the given tag will be removed. + (For example, GigabitEthernet1/0/11, FortyGigabitEthernet1/1/2) . + type: list + elements: str + remove_device_tag_member: + description: Removing the device members from the tag. + type: dict + suboptions: + tag_name: + description: Name of the tag which is to be removed from the device member. + type: str + remove_interface_tag_member: + description: Removing the interface members from the tag. + type: dict + suboptions: + tag_name: + description: Name of the tag which is to be removed from the interface member. + type: str + interface_names_list: + description: Specify the list of interface names to to which the given tag will be removed. + (For example, GigabitEthernet1/0/11, FortyGigabitEthernet1/1/2) . + type: list + elements: str + delete_tag: + description: Deleting the tag from the inventory. + type: dict + suboptions: + tag_name: + description: Name of the tag which is to be deleted. + type: str requirements: - dnacentersdk >= 2.5.5 @@ -668,6 +797,107 @@ interface_name: ["GigabitEthernet1/0/11", FortyGigabitEthernet1/1/1] clear_mac_address_table: True +- name: Create tag in Inventory for device/interface without dynamic rules. + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - create_tag: + tag_name: Test_tag + description: "Give the description of the tag" + system_tag: false + +- name: Create tag in Inventory for device/interface with dynamic rules. + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - create_tag: + tag_name: Test_tag123 + system_tag: false + dynamic_rules: + - member_type: "networkdevice" + rules: + operation: "AND" + items: + - operation: "OR" + items: + - operation: "ILIKE" + name: "hostname" + value: "%switch%" + - operation: "ILIKE" + name: "hostname" + value: "%Router%" + +- name: Update the tag in Inventory for device/interface without dynamic rules. + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - update_tag: + tag_name: Test_tag + description: "Give the description of the tag" + system_tag: False + +- name: Assigning device memebers to the Tag in Inventory. + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - ip_address_list: ["204.1.2.1", "204.1.2.2"] + assign_device_member_to_tag: + tag_name: Test_tag + +- name: Assigning interface memebers to the Tag in Inventory. + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - ip_address_list: ["204.1.2.1", "204.1.2.2"] + assign_interface_member_to_tag: + interface_name_list: ['GigabitEthernet1/0/1','GigabitEthernet1/0/3'] + tag_name: Test_tag + - name: Export Device Details in a CSV file Interface details with IP Address cisco.dnac.inventory_workflow_manager: dnac_host: "{{dnac_host}}" @@ -775,6 +1005,57 @@ add_user_defined_field: name: "Test123" +- name: Removing tag from the device memebers in Inventory. + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - ip_address_list: ["204.1.2.1", "204.1.2.2"] + remove_device_tag_member: + tag_name: Test_tag + +- name: Removing tag from the interface memebers in Inventory. + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - ip_address_list: ["204.1.2.1", "204.1.2.2"] + remove_interface_tag_member: + interface_name_list: ['GigabitEthernet1/0/1','GigabitEthernet1/0/3'] + tag_name: Test_tag + +- name: Deleting tag from the Inventory. + cisco.dnac.inventory_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - delete_tag: + tag_name: Test_tag + """ RETURN = r""" @@ -921,6 +1202,66 @@ def validate_input(self): }, 'resync_retry_count': {'default': 200, 'type': 'int'}, 'resync_retry_interval': {'default': 2, 'type': 'int'}, + }, + 'create_tag': { + 'type': 'dict', + 'tag_name': {'type': 'str'}, + 'system_tag': {'type': 'str'}, + 'description': {'type': 'str'}, + 'dynamic_rules': { + 'type': 'list', + 'member_type': {'type': 'str'}, + 'rules': { + 'type': 'dict', + 'items': { + 'type': 'list', + 'operation': {'type': 'str'}, + 'name': {'type': 'str'}, + 'value': {'type': 'str'}, + }, + }, + }, + }, + 'update_tag': { + 'type': 'dict', + 'tag_name': {'type': 'str'}, + 'system_tag': {'type': 'str'}, + 'description': {'type': 'str'}, + 'dynamic_rules': { + 'type': 'list', + 'member_type': {'type': 'str'}, + 'rules': { + 'type': 'dict', + 'items': { + 'type': 'list', + 'operation': {'type': 'str'}, + 'name': {'type': 'str'}, + 'value': {'type': 'str'}, + }, + }, + }, + }, + 'assign_device_member_to_tag': { + 'type': 'dict', + 'tag_name': {'type': 'str'}, + }, + 'assign_interface_member_to_tag': { + 'type': 'dict', + 'tag_name': {'type': 'str'}, + 'interface_names_list': {'type': 'str'}, + }, + 'remove_device_tag_member': { + 'type': 'dict', + 'tag_name': {'type': 'str'}, + }, + 'remove_interface_tag_member': { + 'type': 'dict', + 'tag_name': {'type': 'str'}, + 'interface_names_list': {'type': 'list', 'elements': 'str'}, + }, + 'delete_tag': { + 'type': 'dict', + 'tag_name': {'type': 'str'}, } } @@ -945,7 +1286,7 @@ def validate_input(self): def get_device_ips_from_config_priority(self): """ Retrieve device IPs based on the configuration. - Args: + Parameters: - self (object): An instance of a class used for interacting with Cisco Cisco Catalyst Center. Returns: list: A list containing device IPs. @@ -2745,7 +3086,7 @@ def clear_mac_address(self, interface_id, deploy_mode, interface_name): def update_interface_detail_of_device(self, device_to_update): """ Update interface details for a device in Cisco Catalyst Center. - Args: + Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_to_update (list): A list of IP addresses of devices to be updated. Returns: @@ -2921,91 +3262,778 @@ def check_device_update_execution_response(self, response, device_ip): return self - def get_want(self, config): + def check_and_get_tag_id(self, name): """ - Get all the device related information from playbook that is needed to be - add/update/delete/resync device in Cisco Catalyst Center. + Check for a tag by name and retrieve its ID. Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - config (dict): A dictionary containing device-related information from the playbook. + name (str): The name of the tag to check and retrieve its ID. Returns: - dict: A dictionary containing the extracted device parameters and other relevant information. + str or None: The ID of the tag if found, or None if the tag does not exist or an error occurs. Description: - Retrieve all the device-related information from the playbook needed for adding, updating, deleting, - or resyncing devices in Cisco Catalyst Center. + This function checks for the existence of a tag in Cisco Catalyst Center by its name and retrieves its ID. + It queries the Cisco Catalyst Center API using the 'get_tag' function to fetch tag details based on the provided name. + If the tag is found, its ID is extracted from the API response. + If the tag is not found or an error occurs during the process, None is returned. """ - want = {} - device_params = self.get_device_params(config) - want["device_params"] = device_params + try: + tag_id = None + response = self.dnac._exec( + family="tag", + function='get_tag', + params={"name": name}, + ) + self.log("Received API response from 'get_tag': {0}".format(str(response)), "DEBUG") + response = response.get('response') - self.want = want - self.msg = "Successfully collected all parameters from the playbook " - self.status = "success" - self.log("Desired State (want): {0}".format(str(self.want)), "INFO") + if not response: + self.log("Did not receive any response for the tag '{0}' so cannot get the id.".format(name), "INFO") + return tag_id - return self + tag_id = response[0].get('id') - def get_diff_merged(self, config): + except Exception as e: + error_msg = "An exception occurred while getting the tag '{0}' details, due to - {1}".format(name, str(e)) + self.log(error_msg, "ERROR") + self.result['changed'] = False + self.result['response'] = error_msg + + return tag_id + + def create_tag(self, tag_params): """ - Merge and process differences between existing devices and desired device configuration in Cisco Catalyst Center. + Create a tag in Cisco Catalyst Center based on the provided parameters. Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - config (dict): A dictionary containing the desired device configuration and relevant information from the playbook. + tag_params (dict): A dictionary containing parameters for creating the tag.It should have the following keys: + - 'tag_name' (str): The name of the tag to be created. + - 'system_tag' (bool): A boolean indicating whether the tag is a system tag (optional, default is False). + - 'dynamic_rules' (list): A list of dictionaries representing dynamic rules for the tag (optional). + Each dictionary in the 'dynamic_rules' list should contain the following keys: + - 'member_type' (str): The type of member for the dynamic rule (optional, default is 'networkdevice'). + - 'rules' (dict): A dictionary containing rules for the dynamic rule. Returns: - object: An instance of the class with updated results and status based on the processing of differences. + self (object): An instance of a class used for interacting with Cisco Catalyst Center. Description: - The function processes the differences and, depending on the changes required, it may add, update, - or resynchronize devices in Cisco Catalyst Center. - The updated results and status are stored in the class instance for further use. + This function creates a tag in Cisco Catalyst Center based on the provided parameters. + It constructs the payload for the tag creation API call using the provided parameters. + If dynamic rules are provided, they are formatted and included in the payload. + The function then checks if a tag with the same name already exists. + If a tag with the same name exists, the function returns without creating the tag. + Otherwise, it calls the create tag API and monitors the task execution status. """ - devices_to_add = self.have["device_not_in_ccc"] - device_type = self.config[0].get("type", "NETWORK_DEVICE") - device_resynced = self.config[0].get("device_resync", False) - device_updated = self.config[0].get("device_updated", False) - device_reboot = self.config[0].get("reboot_device", False) - credential_update = self.config[0].get("credential_update", False) - config['type'] = device_type - if device_type == "FIREPOWER_MANAGEMENT_SYSTEM": - config['http_port'] = self.config[0].get("http_port", "443") + tag_payload = {} + dynamicRules = [] + dynamic_rules = tag_params.get('dynamic_rules') - config['ip_address_list'] = devices_to_add + if not dynamic_rules: + self.log("There is no dynamic roles while creating tag for the device or interface", "INFO") + else: + for rule in dynamic_rules: + dynamic_rule_dict = {} + dynamic_rule_dict['memberType'] = rule.get('member_type', "networkdevice") + dynamic_rule_dict['rules'] = rule.get('rules') + dynamicRules.append(dynamic_rule_dict) - if not config['ip_address_list']: - self.msg = "Devices '{0}' already present in Cisco Catalyst Center".format(self.have['devices_in_playbook']) + system_tag = tag_params.get('system_tag', False) + name = tag_params.get('tag_name') + + tag_payload['name'] = name + tag_payload['systemTag'] = system_tag + if dynamicRules: + tag_payload['dynamicRules'] = dynamicRules + + # Check if tag with given name is already created then we have to return from here only + tag_id = self.check_and_get_tag_id(name) + + if tag_id: + self.status = "success" + self.msg = "Cannot create the tag, a group already exists with the same name '{0}'".format(name) self.log(self.msg, "INFO") - self.result['changed'] = False self.result['response'] = self.msg - else: - # To add the devices in inventory - input_params = self.want.get("device_params") - device_params = input_params.copy() + self.result['changed'] = False + return self - if not device_params['snmpVersion']: - device_params['snmpVersion'] = "v3" - device_params['ipAddress'] = config['ip_address_list'] + try: + # Now need to call the create tag API + response = self.dnac._exec( + family="tag", + function='create_tag', + op_modifies=True, + params=tag_payload, + ) + self.log("Received API response from 'create_tag': {0}".format(str(response)), "DEBUG") - if device_params['snmpVersion'] == "v2": - params_to_remove = ["snmpAuthPassphrase", "snmpAuthProtocol", "snmpMode", "snmpPrivPassphrase", "snmpPrivProtocol", "snmpUserName"] - for param in params_to_remove: - device_params.pop(param, None) + if not (response and isinstance(response, dict)): + self.status = "failed" + self.msg = "Received an empty response from the create tag API which indicates create tag API not executed successfully." + self.result['response'] = self.msg + return self - if not device_params['snmpROCommunity']: + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + + if execution_details.get("isError"): self.status = "failed" - self.msg = "Required parameter 'snmpROCommunity' for adding device with snmmp version v2 is not present" - self.result['msg'] = self.msg + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Failed to create the tag '{0}' for the device/interface due to {1}".format(name, failure_reason) + else: + self.msg = "Failed to clear the create the tag '{0}' for the device/interface".format(name) self.log(self.msg, "ERROR") - return self - else: - if not device_params['snmpMode']: - device_params['snmpMode'] = "AUTHPRIV" + self.result['response'] = self.msg + break + elif 'created successfully' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Successfully created the tag '{0}' for the device/interface.".format(name) + self.log(self.msg, "INFO") + break - if not device_params['cliTransport']: - device_params['cliTransport'] = "ssh" + except Exception as e: + self.status = "failed" + self.msg = "An exception occurred while creating the tag '{0}' for the device/interface due to - {1}".format(name, str(e)) + self.log(self.msg, "ERROR") + self.result['response'] = self.msg - if not device_params['snmpPrivProtocol']: - device_params['snmpPrivProtocol'] = "AES128" + return self + + def update_tag(self, tag_params): + """ + Update a tag in Cisco Catalyst Center based on the provided parameters. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + tag_params (dict): A dictionary containing parameters for updating the tag. + It should have the following keys: + - 'tag_name' (str): The name of the tag to be updated. + - 'system_tag' (bool): A boolean indicating whether the tag is a system tag (optional). + - 'description' (str): A description for the tag (optional). + - 'dynamic_rules' (list): A list of dictionaries representing dynamic rules for the tag (optional). + Each dictionary in the 'dynamic_rules' list should contain the following keys: + - 'member_type' (str): The type of member for the dynamic rule (optional, default is 'networkdevice'). + - 'rules' (dict): A dictionary containing rules for the dynamic rule. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This function updates a tag in Cisco Catalyst Center based on the provided parameters. + It first checks if the tag exists by calling the 'check_and_get_tag_id' method. + If the tag does not exist, the function sets the status to 'failed' and returns. + If the tag exists, the function constructs the payload for the tag update API call using the provided parameters. + If dynamic rules are provided, they are formatted and included in the payload. + The function then calls the update tag API and monitors the task execution status. + """ + + # Get the tag details from Cisco Catalyst Center + name = tag_params.get('tag_name') + tag_id = self.check_and_get_tag_id(name) + + if not tag_id: + self.status = "failed" + self.msg = "Cannot update the tag '{0}' as group is not present in Cisco Catalyst Center.".format(name) + self.log(self.msg, "INFO") + self.result['response'] = self.msg + return self + + response = self.dnac._exec( + family="tag", + function='get_tag', + op_modifies=True, + params={"name": name} + ) + self.log("Received API response from 'get_tag': {0}".format(str(response)), "DEBUG") + response = response.get('response')[0] + + tag_payload = {} + dynamicRules = [] + dynamic_rules = tag_params.get('dynamic_rules') + + if not dynamic_rules: + dynamic_rules = response.get(dynamic_rules) + if not dynamic_rules: + self.log("There is no dynamic roles while updating tag for the device or interface", "INFO") + else: + dynamicRules.append(dynamic_rules) + else: + for rule in dynamic_rules: + dynamic_rule_dict = {} + dynamic_rule_dict['memberType'] = rule.get('member_type', "networkdevice") + dynamic_rule_dict['rules'] = rule.get('rules') + dynamicRules.append(dynamic_rule_dict) + + system_tag = tag_params.get('system_tag') + description = tag_params.get('description') + + if not system_tag: + system_tag = response.get('system_tag') + if not description: + description = response.get('description', "") + + tag_payload['name'] = name + tag_payload['systemTag'] = system_tag + tag_payload['id'] = tag_id + tag_payload['description'] = description + + if dynamicRules: + tag_payload['dynamicRules'] = dynamicRules + + try: + # Now need to call the update tag API + response = self.dnac._exec( + family="tag", + function='update_tag', + op_modifies=True, + params=tag_payload, + ) + self.log("Received API response from 'update_tag': {0}".format(str(response)), "DEBUG") + + if not (response and isinstance(response, dict)): + self.status = "failed" + self.msg = """Received an empty response from the updating the tag '{0}' which indicates tag updation + not executed successfully.""".format(name) + self.result['response'] = self.msg + return self + + response = response.get('response') + task_id = response.get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + + if execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Failed to update the tag '{0}' for the device/interface due to {1}".format(name, failure_reason) + else: + self.msg = "Failed to clear the update the tag '{0}' for the device/interface".format(name) + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + break + elif 'updated successfully' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Successfully updated the tag '{0}' for the device/interface.".format(name) + self.log(self.msg, "INFO") + break + + except Exception as e: + self.status = "failed" + self.msg = "An exception occurred while updating the tag '{0}' for the device/interface due to - {1}".format(name, str(e)) + self.log(self.msg, "WARNING") + self.result['changed'] = False + self.result['response'] = self.msg + + return self + + def add_member_to_tag(self, member_payload, tag_name): + + try: + # Now need to call the create tag API + response = self.dnac._exec( + family="tag", + function='add_members_to_the_tag', + op_modifies=True, + params=member_payload, + ) + self.log("Received API response from 'add_members_to_the_tag': {0}".format(str(response)), "DEBUG") + + if not (response and isinstance(response, dict)): + self.status = "failed" + self.msg = """Received an empty response from the Add member to tag API for the tag '{0}' which + indicates API not executed successfully""".format(tag_name) + self.result['response'] = self.msg + return self + + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + + if execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Failed to assign the tag '{0}' to the member due to {1}".format(tag_name, failure_reason) + else: + self.msg = "Failed to assign the tag '{0}' to the member".format(tag_name) + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + break + elif 'Successfully added' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Successfully executed the task of assigning the tag '{0}' to the member(s) ".format(tag_name) + self.log(self.msg, "INFO") + break + + except Exception as e: + self.status = "failed" + self.msg = "An exception occurred while assigning the tag '{0}' to the member(s) due to - {1}".format(tag_name, str(e)) + self.log(self.msg, "ERROR") + self.result['changed'] = False + self.result['response'] = self.msg + + return self + + def assign_device_member_to_tag(self): + """ + Add member(s) to a tag in Cisco Catalyst Center based on the provided payload. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + member_payload (dict): A dictionary containing the payload for adding member(s) to the tag. + tag_name (str): The name of the tag to which member(s) will be added. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This function adds member(s) to a tag in Cisco Catalyst Center based on the provided payload. + It constructs the payload for the API call using the provided member payload and tag name. + The function then calls the add members to tag API and monitors the task execution status. + """ + + device_member_params = self.config[0].get('assign_device_member_to_tag') + device_ips = self.get_device_ips_from_config_priority() + device_ids = self.get_device_ids(device_ips) + tag_name = device_member_params.get('tag_name') + + if not tag_name: + self.status = "failed" + self.msg = "Required parameter 'tag_name' is missing for adding device member to the tag." + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + return self + + tag_id = self.check_and_get_tag_id(tag_name) + + if not tag_id: + # create the device tag in Cisco Catalyst Center with the given tag name + self.log("The given device tag '{0}' is not created in Cisco Catalyst Center".format(tag_name), "INFO") + tag_params = { + 'name': tag_name + } + self.create_device_tag(tag_params) + + # Since the tag is created so we will fetch the tag_id in order to assign member to tag + tag_id = self.check_and_get_tag_id(tag_name) + + mem_payload = { + 'networkdevice': device_ids + } + + device_member_payload = { + 'id': tag_id, + 'payload': mem_payload + } + + self.add_member_to_tag(device_member_payload, tag_name).check_return_status() + + return self + + def assign_interface_member_to_tag(self): + """ + Assign interface member(s) to a tag in Cisco Catalyst Center based on configuration parameters. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This function assigns interface member(s) to a tag in Cisco Catalyst Center based on configuration parameters. + It retrieves the configuration parameters including tag name, device IPs, and interface names list. + If the tag name or interface names list is missing, the function sets the status to 'failed' and returns. + It then checks if the tag exists in Cisco Catalyst Center, and if not, creates the tag. + Next, it retrieves device IDs corresponding to the provided device IPs. + For each device, it retrieves the interface ID(s) corresponding to the provided interface names. + If interface IDs are found, it constructs the payload for adding interface member(s) to the tag. + The function calls the add member to tag API and monitors the task execution status. + """ + + interface_member_params = self.config[0].get('assign_interface_member_to_tag') + device_ips = self.get_device_ips_from_config_priority() + device_ids = self.get_device_ids(device_ips) + tag_name = interface_member_params.get('tag_name') + interface_names_list = interface_member_params.get('interface_names_list') + + if not tag_name or not interface_names_list: + self.status = "failed" + self.msg = "Required parameter 'tag_name/interface_name' is missing for adding device member to the tag." + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + return self + + tag_id = self.check_and_get_tag_id(tag_name) + if not tag_id: + # create the tag in Cisco Catalyst Center with the given tag name + self.log("The given tag '{0}' is not created in Cisco Catalyst Center".format(tag_name), "INFO") + tag_params = { + 'name': tag_name + } + self.create_device_tag(tag_params) + + # Since the tag is created so we will fetch the tag_id in order to assign member to tag + tag_id = self.check_and_get_tag_id(tag_name) + + interface_ids = [] + for device_id in device_ids: + for interface_name in interface_names_list: + interface_id = self.get_interface_from_id_and_name(device_id, interface_name) + if interface_id: + interface_ids.append(interface_id) + + if not interface_ids: + self.status = "failed" + self.msg = "Doesnot receive any interface ids so cannot perfrom assigning member to tag operation" + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + return self + + mem_payload = { + 'interface': interface_ids + } + + interface_member_payload = { + 'id': tag_id, + 'payload': mem_payload + } + self.add_member_to_tag(interface_member_payload, tag_name).check_return_status() + + return self + + def remove_tag_member(self, remove_tag_member_payload, tag_name): + """ + Remove tag from member(s) in Cisco Catalyst Center based on the provided payload. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + remove_tag_member_payload (dict): A dictionary containing the payload for removing tag from member(s). + tag_name (str): The name of the tag to be removed from the member(s). + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This function removes tag from member(s) in Cisco Catalyst Center based on the provided payload. + It constructs the payload for the API call using the provided remove tag member payload and tag name. + The function then calls the remove tag member API and monitors the task execution status. + """ + + try: + response = self.dnac._exec( + family="tag", + function='remove_tag_member', + params=remove_tag_member_payload, + ) + + self.log("Received API response from 'remove_tag_member': {0}".format(str(response)), "DEBUG") + + response = response.get('response') + if not response: + self.status = "failed" + self.log("Did not receive any response for the tag '{0}' so cannot remove tag from member.".format(tag_name), "INFO") + return self + + task_id = response.get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + + if execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Failed to remove the tag '{0}' from the member due to {1}".format(tag_name, failure_reason) + else: + self.msg = "Failed to remove the tag '{0}' from the member".format(tag_name) + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + break + elif 'deleted successfully' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Successfully executed the task of removing the tag '{0}' to the member(s) ".format(tag_name) + self.log(self.msg, "INFO") + break + + except Exception as e: + self.status = "failed" + self.msg = "An exception occurred while removing the tag '{0}' from the member, due to - {1}".format(tag_name, str(e)) + self.log(self.msg, "ERROR") + self.result['changed'] = False + self.result['response'] = self.msg + + return self + + def remove_device_tag_member(self, tag_name): + """ + Remove device member(s) from a tag in Cisco Catalyst Center based on the provided tag name. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + tag_name (str): The name of the tag from which device member(s) will be removed. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This function removes device member(s) from a tag in Cisco Catalyst Center based on the provided tag name. + It retrieves the tag ID corresponding to the given tag name, if not found, the function sets the status to + 'failed' and returns. Then retrieves the device IPs from the configuration.For each device IP, it retrieves + the device ID and constructs the payload for removing device member(s) from the tag. + The function calls the remove tag member API and monitors the task execution status. + """ + + try: + # Get the tag id with the given tag name + tag_id = self.check_and_get_tag_id(tag_name) + + if not tag_id: + self.msg = """Given tag '{0}' not present in Cisco Catalyst Center so cannot perform the remove tag from + the member operation""".format(tag_name) + self.log(self.msg, "INFO") + self.result['response'] = self.msg + return self + + member_ips = self.get_device_ips_from_config_priority() + + for member_ip in member_ips: + member_id = self.get_device_ids([member_ip]) + remove_tag_member_payload = { + 'member_id': member_id[0], + 'id': tag_id + } + self.remove_tag_member(remove_tag_member_payload, tag_name).check_return_status() + self.log("Tag '{0}' successfully removed from the member {1} in Cisco Catalyst Center".format(tag_name, member_ip), "INFO") + + except Exception as e: + self.status = "failed" + self.msg = "An exception occurred while removing the member from the tag '{0}' details, due to - {1}".format(tag_name, str(e)) + self.log(self.msg, "ERROR") + self.result['changed'] = False + self.result['response'] = self.msg + + return self + + def remove_interface_tag_member(self, tag_name, interface_name_list): + """ + Remove interface member(s) from a tag in Cisco Catalyst Center based on the provided tag name and interface names. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + tag_name (str): The name of the tag from which interface member(s) will be removed. + interface_name_list (list of str): A list containing the names of interfaces to be removed from the tag. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This function removes interface member(s) from a tag in Cisco Catalyst Center based on the provided tag name and interface names. + It retrieves the tag ID corresponding to the given tag name and if not found, the function sets the status to 'failed' and + returns. Then retrieves the device IPs from the configuration and gets their corresponding device IDs. + For each device ID and interface name combination, it retrieves the interface ID and constructs the payload for removing + interface member(s) from the tag. The function calls the remove tag member API and monitors the task execution status. + """ + + try: + # Get the tag id with the given tag name + tag_id = self.check_and_get_tag_id(tag_name) + + if not tag_id: + self.msg = """Given tag '{0}' not present in Cisco Catalyst Center so cannot perform the remove tag from + the member operation""".format(tag_name) + self.log(self.msg, "INFO") + self.result['response'] = self.msg + return self + + device_ips = self.get_device_ips_from_config_priority() + device_ids = self.get_device_ids(device_ips) + interface_ids = [] + for device_id in device_ids: + for interface_name in interface_name_list: + interface_id = self.get_interface_from_id_and_name(device_id, interface_name) + if interface_id: + interface_ids.append(interface_id) + + if not interface_ids: + self.status = "failed" + self.msg = "Doesnot receive any interface ids so cannot perfrom remove tag from interface member task." + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + return self + + for interface_id in interface_ids: + remove_tag_member_payload = { + 'member_id': interface_id, + 'id': tag_id + } + self.remove_tag_member(remove_tag_member_payload, tag_name).check_return_status() + self.log("Tag '{0}' successfully removed from the member in Cisco Catalyst Center".format(tag_name), "INFO") + + except Exception as e: + self.status = "failed" + self.msg = "An exception occurred while removing the member from the tag '{0}' details, due to - {1}".format(tag_name, str(e)) + self.log(self.msg, "ERROR") + self.result['changed'] = False + self.result['response'] = self.msg + + return self + + def delete_tag(self, tag_name): + """ + Delete a tag from Cisco Catalyst Center based on the provided tag name. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + tag_name (str): The name of the tag to be deleted. + Returns: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + Description: + This function deletes a tag from Cisco Catalyst Center based on the provided tag name. + It retrieves the tag ID corresponding to the given tag name and if not found, the function + sets the status to 'success' and logs an informational message indicating that the tag is already deleted. + It then constructs the payload for deleting the tag and calls the delete tag API. + The function monitors the task execution status and logs appropriate messages based on the outcome. + """ + try: + # Get the tag id with the given tag name + tag_id = self.check_and_get_tag_id(tag_name) + if not tag_id: + self.status = "success" + self.msg = "Tag '{0}' already deleted from Cisco Catalyst Center".format(tag_name) + self.log(self.msg, "INFO") + self.result['changed'] = False + self.result['response'] = self.msg + return self + + delete_tag_params = { + "id": tag_id + } + + response = self.dnac._exec( + family="tag", + function='delete_tag', + params=delete_tag_params, + ) + + self.log("Received API response from 'delete_tag': {0}".format(str(response)), "DEBUG") + response = response.get('response') + if not response: + self.status = "failed" + self.log("Did not receive any response for the tag '{0}' so cannot delete tag from Cisco Catalyst Center.".format(tag_name), "INFO") + return self + + task_id = response.get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + + if execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Failed to delete the tag '{0}' from Cisco Catalyst Center due to {1}".format(tag_name, failure_reason) + else: + self.msg = "Failed to delete the tag '{0}' from Cisco Catalyst Center".format(tag_name) + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + break + elif 'deleted successfully' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.msg = "Successfully executed the task of deleting the tag '{0}' Cisco Catalyst Center.".format(tag_name) + self.log(self.msg, "INFO") + break + + except Exception as e: + self.status = "failed" + self.msg = "An exception occurred while deleting the tag '{0}' from Cisco Catalyst Center, due to - {1}".format(tag_name, str(e)) + self.log(self.msg, "ERROR") + self.result['changed'] = False + self.result['response'] = self.msg + + return self + + def get_want(self, config): + """ + Get all the device related information from playbook that is needed to be + add/update/delete/resync device in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + config (dict): A dictionary containing device-related information from the playbook. + Returns: + dict: A dictionary containing the extracted device parameters and other relevant information. + Description: + Retrieve all the device-related information from the playbook needed for adding, updating, deleting, + or resyncing devices in Cisco Catalyst Center. + """ + + want = {} + device_params = self.get_device_params(config) + want["device_params"] = device_params + + self.want = want + self.msg = "Successfully collected all parameters from the playbook " + self.status = "success" + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") + + return self + + def get_diff_merged(self, config): + """ + Merge and process differences between existing devices and desired device configuration in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + config (dict): A dictionary containing the desired device configuration and relevant information from the playbook. + Returns: + object: An instance of the class with updated results and status based on the processing of differences. + Description: + The function processes the differences and, depending on the changes required, it may add, update, + or resynchronize devices in Cisco Catalyst Center. + The updated results and status are stored in the class instance for further use. + """ + devices_to_add = self.have["device_not_in_ccc"] + device_type = self.config[0].get("type", "NETWORK_DEVICE") + device_resynced = self.config[0].get("device_resync", False) + device_updated = self.config[0].get("device_updated", False) + device_reboot = self.config[0].get("reboot_device", False) + credential_update = self.config[0].get("credential_update", False) + + config['type'] = device_type + if device_type == "FIREPOWER_MANAGEMENT_SYSTEM": + config['http_port'] = self.config[0].get("http_port", "443") + + config['ip_address_list'] = devices_to_add + + if not config['ip_address_list']: + self.msg = "Devices '{0}' already present in Cisco Catalyst Center".format(self.have['devices_in_playbook']) + self.log(self.msg, "INFO") + self.result['changed'] = False + self.result['response'] = self.msg + else: + # To add the devices in inventory + input_params = self.want.get("device_params") + device_params = input_params.copy() + + if not device_params['snmpVersion']: + device_params['snmpVersion'] = "v3" + device_params['ipAddress'] = config['ip_address_list'] + + if device_params['snmpVersion'] == "v2": + params_to_remove = ["snmpAuthPassphrase", "snmpAuthProtocol", "snmpMode", "snmpPrivPassphrase", "snmpPrivProtocol", "snmpUserName"] + for param in params_to_remove: + device_params.pop(param, None) + + if not device_params['snmpROCommunity']: + self.status = "failed" + self.msg = "Required parameter 'snmpROCommunity' for adding device with snmmp version v2 is not present" + self.result['msg'] = self.msg + self.log(self.msg, "ERROR") + return self + else: + if not device_params['snmpMode']: + device_params['snmpMode'] = "AUTHPRIV" + + if not device_params['cliTransport']: + device_params['cliTransport'] = "ssh" + + if not device_params['snmpPrivProtocol']: + device_params['snmpPrivProtocol'] = "AES128" if device_params['snmpPrivProtocol'] == "AES192": device_params['snmpPrivProtocol'] = "CISCOAES192" @@ -3364,6 +4392,24 @@ def get_diff_merged(self, config): if self.config[0].get('provision_wireless_device'): self.provisioned_wireless_devices().check_return_status() + # Create the tag for the device or interface + if self.config[0].get('create_tag'): + tag_params = self.config[0].get('create_tag') + self.create_tag(tag_params).check_return_status() + + # Update the tag for the device or interface + if self.config[0].get('update_tag'): + tag_params = self.config[0].get('update_tag') + self.update_tag(tag_params).check_return_status() + + # Assigning the device members to the tag + if self.config[0].get('assign_device_member_to_tag'): + self.assign_device_member_to_tag().check_return_status() + + # Assigning the interface members of the devices to the tag + if self.config[0].get('assign_interface_member_to_tag'): + self.assign_interface_member_to_tag().check_return_status() + if device_resynced: self.resync_devices().check_return_status() @@ -3392,6 +4438,52 @@ def get_diff_deleted(self, config): device_to_delete = self.get_device_ips_from_config_priority() self.result['msg'] = [] + if self.config[0].get('remove_device_tag_member'): + tag_params = self.config[0].get('remove_device_tag_member') + tag_name = tag_params.get('tag_name') + + if not tag_name: + self.status = "failed" + self.msg = """Cannot perform the remove task of tag from device member, 'tag_name' + is required to perform this task """ + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + return self + + self.remove_device_tag_member(tag_name).check_return_status() + return self + + if self.config[0].get('remove_interface_tag_member'): + tag_params = self.config[0].get('remove_interface_tag_member') + tag_name = tag_params.get('tag_name') + interface_name_list = tag_params.get('interface_names_list') + + if not tag_name or not interface_name_list: + self.status = "failed" + self.msg = """Cannot perform the remove task of tag from interface member, 'tag_name/interface_name' + is required to perform this task """ + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + return self + + self.remove_interface_tag_member(tag_name, interface_name_list).check_return_status() + return self + + if self.config[0].get('delete_tag'): + tag_params = self.config[0].get('delete_tag') + tag_name = tag_params.get('tag_name') + + if not tag_name: + self.status = "failed" + self.msg = """Cannot perform the remove task of tag from device member, 'tag_name' + is required to perform this task """ + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + return self + + self.delete_tag(tag_name).check_return_status() + return self + if self.config[0].get('add_user_defined_field'): udf_field_list = self.config[0].get('add_user_defined_field') for udf in udf_field_list: @@ -3557,6 +4649,8 @@ def verify_diff_merged(self, config): if not devices_to_add: self.status = "success" + if not self.have['devices_in_playbook']: + msg = "Device addition task not needed as devices are already present in Cisco Caalyst Center." msg = """Requested device(s) '{0}' have been successfully added to the Cisco Catalyst Center and their addition has been verified.""".format(str(self.have['devices_in_playbook'])) self.log(msg, "INFO") From 540d82f822005c6ae5d1b163d089247661f5adac Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 5 Mar 2024 16:31:16 +0530 Subject: [PATCH 02/17] add space after comma in playbook --- playbooks/inventory_workflow_manager.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playbooks/inventory_workflow_manager.yml b/playbooks/inventory_workflow_manager.yml index 11f58ce939..dc2dfe7077 100644 --- a/playbooks/inventory_workflow_manager.yml +++ b/playbooks/inventory_workflow_manager.yml @@ -69,13 +69,13 @@ assign_device_member_to_tag: tag_name: Pre_test_check1 assign_interface_member_to_tag: - interface_names_list: ['GigabitEthernet1/0/1','GigabitEthernet1/0/3'] + interface_names_list: ['GigabitEthernet1/0/1', 'GigabitEthernet1/0/3'] tag_name: Pre_test_check1 remove_device_tag_member: tag_name: March_4th remove_interface_tag_member: - interface_names_list: ['GigabitEthernet1/0/1','GigabitEthernet1/0/3'] + interface_names_list: ['GigabitEthernet1/0/1', 'GigabitEthernet1/0/3'] tag_name: March_4th delete_tag: tag_name: string_device_tag From 682cfd3cf15ebc34dbd11d909ce956ef4a025aa2 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Thu, 7 Mar 2024 22:14:44 +0530 Subject: [PATCH 03/17] Updated the changelog.yaml about the stackswitch getting converted to normal switch --- changelogs/changelog.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 909e4cdcff..64df79e0e0 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -827,3 +827,5 @@ releases: attributes 'clear_mac_address_table', 'device_ip', 'resync_retry_count', 'resync_retry_interval', 'reprovision_wired_device', 'provision_wireless_device' were added. Renamed argument from 'ip_address' to 'ip_address_list'. + - pnp_workflow_manager - Adding fix for Stackswitch getting changed to normal switch post editing the device's info. + - pnp_intent - Adding fix for Stackswitch getting changed to normal switch post editing the device's info. From 9d9f353347b3113f0895155151a38eda1da11b35 Mon Sep 17 00:00:00 2001 From: bvargasre Date: Thu, 7 Mar 2024 12:51:55 -0600 Subject: [PATCH 04/17] Update supported versions in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f1ab07480c..2a3014b43c 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The following table shows the supported versions. | 2.2.2.3 | 3.3.1 | 2.3.3 | | 2.2.3.3 | 6.4.0 | 2.4.11 | | 2.3.3.0 | 6.6.4 | 2.5.5 | -| 2.3.5.3 | 6.11.0 | 2.6.0 | +| 2.3.5.3 | 6.12.0 | 2.6.0 | If your Ansible collection is older please consider updating it first. From 8c9840139b07cda03f069c83e636052d2408ad8f Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 12 Mar 2024 06:51:55 +0000 Subject: [PATCH 05/17] Adding fix for nonetype error in global credentials --- playbooks/discovery_intent.yml | 6 +- playbooks/discovery_workflow_manager.yml | 6 +- plugins/modules/discovery_intent.py | 117 ++++++++++++++--- plugins/modules/discovery_workflow_manager.py | 119 +++++++++++++++--- 4 files changed, 211 insertions(+), 37 deletions(-) diff --git a/playbooks/discovery_intent.yml b/playbooks/discovery_intent.yml index db503456bf..7be8b1076f 100644 --- a/playbooks/discovery_intent.yml +++ b/playbooks/discovery_intent.yml @@ -99,6 +99,7 @@ discovery_type: "SINGLE" ip_address_list: - 204.1.2.5 + use_global_credentials: False discovery_specific_credentials: cli_credentials_list: - username: cisco @@ -123,10 +124,9 @@ privacy_type: AES256 privacy_password: Lablab#1234 global_cli_len: 3 - use_global_cred: False protocol_order: ssh - - name: Execute discovery devices using MULTI RANGE with various discovery specific credentials + - name: Execute discovery devices using MULTI RANGE with various discovery specific credentials and all global credentials (max 5 allowed) cisco.dnac.discovery_intent: <<: *dnac_login state: merged @@ -164,7 +164,7 @@ records_to_return: 1000 snmp_version: v2 - - name: Execute discovery devices using CDP/LLDP/CIDR + - name: Execute discovery devices using CDP/LLDP/CIDR leveraging discovery specific credentials and all the global credentials cisco.dnac.discovery_intent: <<: *dnac_login state: merged diff --git a/playbooks/discovery_workflow_manager.yml b/playbooks/discovery_workflow_manager.yml index 187e9974f5..166bcf0f0f 100644 --- a/playbooks/discovery_workflow_manager.yml +++ b/playbooks/discovery_workflow_manager.yml @@ -99,6 +99,7 @@ discovery_type: "SINGLE" ip_address_list: - 204.1.2.5 + use_global_credentials: False discovery_specific_credentials: cli_credentials_list: - username: cisco @@ -123,10 +124,9 @@ privacy_type: AES256 privacy_password: Lablab#1234 global_cli_len: 3 - use_global_cred: False protocol_order: ssh - - name: Execute discovery devices using MULTI RANGE with various discovery specific credentials + - name: Execute discovery devices using MULTI RANGE with various discovery specific credentials and all global credentials (max 5 allowed) cisco.dnac.discovery_workflow_manager: <<: *dnac_login state: merged @@ -164,7 +164,7 @@ records_to_return: 1000 snmp_version: v2 - - name: Execute discovery devices using CDP/LLDP/CIDR + - name: Execute discovery devices using CDP/LLDP/CIDR leveraging discovery specific credentials and all the global credentials cisco.dnac.discovery_workflow_manager: <<: *dnac_login state: merged diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 0c879cf8b2..208808f1c8 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -261,26 +261,20 @@ snmp_v2_read_credential_list: description: - List of Global SNMP V2 Read credentials to be used during device discovery. - - It's recommended to create device credentials with both a unique username and a clear description for easy identification. + - It's recommended to create device credentials with a clear description for easy identification. type: list elements: dict suboptions: - username: - description: Username for SNMP Read authentication, mandatory when using global SNMP credentials. - type: str description: description: Name of the SNMP Read credential, mandatory when using global SNMP credentials. type: str snmp_v2_write_credential_list: description: - List of Global SNMP V2 Write credentials to be used during device discovery. - - It's recommended to create device credentials with both a unique username and a clear description for easy identification. + - It's recommended to create device credentials with a clear description for easy identification. type: list elements: dict suboptions: - username: - description: Username for SNMP Write authentication, mandatory when using global SNMP credentials. - type: str description: description: Name of the SNMP Write credential, mandatory when using global SNMP credentials. type: str @@ -358,7 +352,7 @@ """ EXAMPLES = r""" -- name: Execute discovery devices with both global credentials and discovery specific credentials +- name: Execute discovery of devices with both global credentials and discovery specific credentials cisco.dnac.discovery_intent: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" @@ -423,10 +417,8 @@ username: string snmp_v2_read_credential_list: - description: string - username: string snmp_v2_write_credential_list: - description: string - username: string net_conf_port_list: - description: string start_index: integer @@ -435,7 +427,7 @@ retry: integer timeout: integer -- name: Execute discovery devices with discovery specific credentials only +- name: Execute discovery of devices with discovery specific credentials only cisco.dnac.discovery_intent: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" @@ -492,6 +484,80 @@ retry: integer timeout: integer +- name: Execute discovery of devices with global credentials only + cisco.dnac.discovery_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + dnac_log_level: "{{dnac_log_level}}" + state: merged + config_verify: True + config: + - discovery_name: string + discovery_type: string + ip_address_list: list + ip_filter_list: list + cdp_level: string + lldp_level: string + prefered_mgmt_ip_method: string + global_credentials: + cli_credentials_list: + - description: string + username: string + http_read_credential_list: + - description: string + username: string + http_write_credential_list: + - description: string + username: string + snmp_v3_credential_list: + - description: string + username: string + snmp_v2_read_credential_list: + - description: string + snmp_v2_write_credential_list: + - description: string + net_conf_port_list: + - description: string + start_index: integer + records_to_return: integer + protocol_order: string + retry: integer + timeout: integer + +- name: Execute discovery of devices with all the global credentials (max 5 allowed) + cisco.dnac.discovery_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + dnac_log_level: "{{dnac_log_level}}" + state: merged + config_verify: True + config: + - discovery_name: string + discovery_type: string + ip_address_list: list + ip_filter_list: list + cdp_level: string + lldp_level: string + prefered_mgmt_ip_method: string + start_index: integer + records_to_return: integer + protocol_order: string + retry: integer + timeout: integer + use_global_credentials: True + - name: Delete disovery by name cisco.dnac.discovery_intent: dnac_host: "{{dnac_host}}" @@ -687,6 +753,9 @@ def handle_global_credentials(self, response=None): if not isinstance(cli_credentials_list, list): msg = "Global CLI credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) + if response.get("cliCredential") is None: + msg = "No Global CLI credentials are present in the Cisco Catalyst Center" + self.discovery_specific_cred_failure(msg=msg) if len(cli_credentials_list) > 0: global_credentials_all["cliCredential"] = [] cred_len = len(cli_credentials_list) @@ -707,6 +776,9 @@ def handle_global_credentials(self, response=None): if not isinstance(http_read_credential_list, list): msg = "Global HTTP read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) + if response.get("httpsRead") is None: + msg = "No Global HTTP read credentials are present in the Cisco Catalyst Center" + self.discovery_specific_cred_failure(msg=msg) if len(http_read_credential_list) > 0: global_credentials_all["httpsRead"] = [] cred_len = len(http_read_credential_list) @@ -727,6 +799,9 @@ def handle_global_credentials(self, response=None): if not isinstance(http_write_credential_list, list): msg = "Global HTTP write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) + if response.get("httpsWrite") is None: + msg = "No Global HTTP write credentials are present in the Cisco Catalyst Center" + self.discovery_specific_cred_failure(msg=msg) if len(http_write_credential_list) > 0: global_credentials_all["httpsWrite"] = [] cred_len = len(http_write_credential_list) @@ -747,6 +822,9 @@ def handle_global_credentials(self, response=None): if not isinstance(snmp_v2_read_credential_list, list): msg = "Global SNMPV2 read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) + if response.get("snmpV2cRead") is None: + msg = "No Global SNMPV2 read credentials are present in the Cisco Catalyst Center" + self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_read_credential_list) > 0: global_credentials_all["snmpV2cRead"] = [] cred_len = len(snmp_v2_read_credential_list) @@ -759,7 +837,7 @@ def handle_global_credentials(self, response=None): global_credentials_all["snmpV2cRead"].append(snmp.get("id")) global_credentials_all["snmpV2cRead"] = global_credentials_all["snmpV2cRead"][:cred_len] else: - msg = "Kindly ensure you include both the description and the username for the Global SNMPV2 Read \ + msg = "Kindly ensure you include the description for the Global SNMPV2 Read \ credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) @@ -768,6 +846,9 @@ def handle_global_credentials(self, response=None): if not isinstance(snmp_v2_write_credential_list, list): msg = "Global SNMPV2 write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) + if response.get("snmpV2cWrite") is None: + msg = "No Global SNMPV2 write credentials are present in the Cisco Catalyst Center" + self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_write_credential_list) > 0: global_credentials_all["snmpV2cWrite"] = [] cred_len = len(snmp_v2_write_credential_list) @@ -780,7 +861,7 @@ def handle_global_credentials(self, response=None): global_credentials_all["snmpV2cWrite"].append(snmp.get("id")) global_credentials_all["snmpV2cWrite"] = global_credentials_all["snmpV2cWrite"][:cred_len] else: - msg = "Kindly ensure you include both the description and the username for the Global SNMPV2 credential to discover the devices" + msg = "Kindly ensure you include the description for the Global SNMPV2 write credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) snmp_v3_credential_list = global_credentials.get('snmp_v3_credential_list') @@ -788,6 +869,9 @@ def handle_global_credentials(self, response=None): if not isinstance(snmp_v3_credential_list, list): msg = "Global SNMPV3 write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) + if response.get("snmpV3") is None: + msg = "No Global SNMPV3 credentials are present in the Cisco Catalyst Center" + self.discovery_specific_cred_failure(msg=msg) if len(snmp_v3_credential_list) > 0: global_credentials_all["snmpV3"] = [] cred_len = len(snmp_v3_credential_list) @@ -809,6 +893,9 @@ def handle_global_credentials(self, response=None): if not isinstance(net_conf_port_list, list): msg = "Global net Conf Ports be passed as a list" self.discovery_specific_cred_failure(msg=msg) + if response.get("netconfCredential") is None: + msg = "No Global Net Conf Ports are present in the Cisco Catalyst Center" + self.discovery_specific_cred_failure(msg=msg) if len(net_conf_port_list) > 0: global_credentials_all["netconfCredential"] = [] cred_len = len(net_conf_port_list) @@ -821,7 +908,7 @@ def handle_global_credentials(self, response=None): global_credentials_all["netconfCredential"].append(netconf.get("id")) global_credentials_all["netconfCredential"] = global_credentials_all["netconfCredential"][:cred_len] else: - msg = "Please provide description of the Global Netconf port to be used" + msg = "Please provide valid description of the Global Netconf port to be used" self.discovery_specific_cred_failure(msg=msg) self.log("Fetched Global credentials IDs are {0}".format(global_credentials_all), "INFO") diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index 6d23ef7222..189ef45736 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -261,26 +261,20 @@ snmp_v2_read_credential_list: description: - List of Global SNMP V2 Read credentials to be used during device discovery. - - It's recommended to create device credentials with both a unique username and a clear description for easy identification. + - It's recommended to create device credentials with a clear description for easy identification. type: list elements: dict suboptions: - username: - description: Username for SNMP Read authentication, mandatory when using global SNMP credentials. - type: str description: description: Name of the SNMP Read credential, mandatory when using global SNMP credentials. type: str snmp_v2_write_credential_list: description: - List of Global SNMP V2 Write credentials to be used during device discovery. - - It's recommended to create device credentials with both a unique username and a clear description for easy identification. + - It's recommended to create device credentials with a clear description for easy identification. type: list elements: dict suboptions: - username: - description: Username for SNMP Write authentication, mandatory when using global SNMP credentials. - type: str description: description: Name of the SNMP Write credential, mandatory when using global SNMP credentials. type: str @@ -358,8 +352,8 @@ """ EXAMPLES = r""" -- name: Execute discovery devices with both global credentials and discovery specific credentials - cisco.dnac.discovery_intent: +- name: Execute discovery of devices with both global credentials and discovery specific credentials + cisco.dnac.discovery_workflow_manager: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" dnac_password: "{{dnac_password}}" @@ -423,10 +417,8 @@ username: string snmp_v2_read_credential_list: - description: string - username: string snmp_v2_write_credential_list: - description: string - username: string net_conf_port_list: - description: string start_index: integer @@ -435,7 +427,7 @@ retry: integer timeout: integer -- name: Execute discovery devices with discovery specific credentials only +- name: Execute discovery of devices with discovery specific credentials only cisco.dnac.discovery_workflow_manager: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" @@ -492,6 +484,80 @@ retry: integer timeout: integer +- name: Execute discovery of devices with global credentials only + cisco.dnac.discovery_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + dnac_log_level: "{{dnac_log_level}}" + state: merged + config_verify: True + config: + - discovery_name: string + discovery_type: string + ip_address_list: list + ip_filter_list: list + cdp_level: string + lldp_level: string + prefered_mgmt_ip_method: string + global_credentials: + cli_credentials_list: + - description: string + username: string + http_read_credential_list: + - description: string + username: string + http_write_credential_list: + - description: string + username: string + snmp_v3_credential_list: + - description: string + username: string + snmp_v2_read_credential_list: + - description: string + snmp_v2_write_credential_list: + - description: string + net_conf_port_list: + - description: string + start_index: integer + records_to_return: integer + protocol_order: string + retry: integer + timeout: integer + +- name: Execute discovery of devices with all the global credentials (max 5 allowed) + cisco.dnac.discovery_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + dnac_log_level: "{{dnac_log_level}}" + state: merged + config_verify: True + config: + - discovery_name: string + discovery_type: string + ip_address_list: list + ip_filter_list: list + cdp_level: string + lldp_level: string + prefered_mgmt_ip_method: string + start_index: integer + records_to_return: integer + protocol_order: string + retry: integer + timeout: integer + use_global_credentials: True + - name: Delete disovery by name cisco.dnac.discovery_workflow_manager: dnac_host: "{{dnac_host}}" @@ -687,6 +753,9 @@ def handle_global_credentials(self, response=None): if not isinstance(cli_credentials_list, list): msg = "Global CLI credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) + if response.get("cliCredential") is None: + msg = "No Global CLI credentials are present in the Cisco Catalyst Center" + self.discovery_specific_cred_failure(msg=msg) if len(cli_credentials_list) > 0: global_credentials_all["cliCredential"] = [] cred_len = len(cli_credentials_list) @@ -707,6 +776,9 @@ def handle_global_credentials(self, response=None): if not isinstance(http_read_credential_list, list): msg = "Global HTTP read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) + if response.get("httpsRead") is None: + msg = "No Global HTTP read credentials are present in the Cisco Catalyst Center" + self.discovery_specific_cred_failure(msg=msg) if len(http_read_credential_list) > 0: global_credentials_all["httpsRead"] = [] cred_len = len(http_read_credential_list) @@ -727,6 +799,9 @@ def handle_global_credentials(self, response=None): if not isinstance(http_write_credential_list, list): msg = "Global HTTP write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) + if response.get("httpsWrite") is None: + msg = "No Global HTTP write credentials are present in the Cisco Catalyst Center" + self.discovery_specific_cred_failure(msg=msg) if len(http_write_credential_list) > 0: global_credentials_all["httpsWrite"] = [] cred_len = len(http_write_credential_list) @@ -747,6 +822,9 @@ def handle_global_credentials(self, response=None): if not isinstance(snmp_v2_read_credential_list, list): msg = "Global SNMPV2 read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) + if response.get("snmpV2cRead") is None: + msg = "No Global SNMPV2 read credentials are present in the Cisco Catalyst Center" + self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_read_credential_list) > 0: global_credentials_all["snmpV2cRead"] = [] cred_len = len(snmp_v2_read_credential_list) @@ -759,7 +837,7 @@ def handle_global_credentials(self, response=None): global_credentials_all["snmpV2cRead"].append(snmp.get("id")) global_credentials_all["snmpV2cRead"] = global_credentials_all["snmpV2cRead"][:cred_len] else: - msg = "Kindly ensure you include both the description and the username for the Global SNMPV2 Read \ + msg = "Kindly ensure you include the description for the Global SNMPV2 Read \ credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) @@ -768,6 +846,9 @@ def handle_global_credentials(self, response=None): if not isinstance(snmp_v2_write_credential_list, list): msg = "Global SNMPV2 write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) + if response.get("snmpV2cWrite") is None: + msg = "No Global SNMPV2 write credentials are present in the Cisco Catalyst Center" + self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_write_credential_list) > 0: global_credentials_all["snmpV2cWrite"] = [] cred_len = len(snmp_v2_write_credential_list) @@ -780,7 +861,7 @@ def handle_global_credentials(self, response=None): global_credentials_all["snmpV2cWrite"].append(snmp.get("id")) global_credentials_all["snmpV2cWrite"] = global_credentials_all["snmpV2cWrite"][:cred_len] else: - msg = "Kindly ensure you include both the description and the username for the Global SNMPV2 credential to discover the devices" + msg = "Kindly ensure you include the description for the Global SNMPV2 write credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) snmp_v3_credential_list = global_credentials.get('snmp_v3_credential_list') @@ -788,6 +869,9 @@ def handle_global_credentials(self, response=None): if not isinstance(snmp_v3_credential_list, list): msg = "Global SNMPV3 write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) + if response.get("snmpV3") is None: + msg = "No Global SNMPV3 credentials are present in the Cisco Catalyst Center" + self.discovery_specific_cred_failure(msg=msg) if len(snmp_v3_credential_list) > 0: global_credentials_all["snmpV3"] = [] cred_len = len(snmp_v3_credential_list) @@ -809,6 +893,9 @@ def handle_global_credentials(self, response=None): if not isinstance(net_conf_port_list, list): msg = "Global net Conf Ports be passed as a list" self.discovery_specific_cred_failure(msg=msg) + if response.get("netconfCredential") is None: + msg = "No Global Net Conf Ports are present in the Cisco Catalyst Center" + self.discovery_specific_cred_failure(msg=msg) if len(net_conf_port_list) > 0: global_credentials_all["netconfCredential"] = [] cred_len = len(net_conf_port_list) @@ -821,7 +908,7 @@ def handle_global_credentials(self, response=None): global_credentials_all["netconfCredential"].append(netconf.get("id")) global_credentials_all["netconfCredential"] = global_credentials_all["netconfCredential"][:cred_len] else: - msg = "Please provide description of the Global Netconf port to be used" + msg = "Please provide valid description of the Global Netconf port to be used" self.discovery_specific_cred_failure(msg=msg) self.log("Fetched Global credentials IDs are {0}".format(global_credentials_all), "INFO") From 89a5c294129869af9c5629ffeb28a43391048738 Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 12 Mar 2024 10:19:42 +0000 Subject: [PATCH 06/17] Adding fix for nonetype error in global credentials --- plugins/modules/discovery_intent.py | 24 +++++++++---------- plugins/modules/discovery_workflow_manager.py | 24 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 208808f1c8..cc06831ff0 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -754,7 +754,7 @@ def handle_global_credentials(self, response=None): msg = "Global CLI credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if response.get("cliCredential") is None: - msg = "No Global CLI credentials are present in the Cisco Catalyst Center" + msg = "Global CLI credentials are not present in the Cisco Catalyst Center" self.discovery_specific_cred_failure(msg=msg) if len(cli_credentials_list) > 0: global_credentials_all["cliCredential"] = [] @@ -777,7 +777,7 @@ def handle_global_credentials(self, response=None): msg = "Global HTTP read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if response.get("httpsRead") is None: - msg = "No Global HTTP read credentials are present in the Cisco Catalyst Center" + msg = "Global HTTP read credentials are not present in the Cisco Catalyst Center" self.discovery_specific_cred_failure(msg=msg) if len(http_read_credential_list) > 0: global_credentials_all["httpsRead"] = [] @@ -800,7 +800,7 @@ def handle_global_credentials(self, response=None): msg = "Global HTTP write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if response.get("httpsWrite") is None: - msg = "No Global HTTP write credentials are present in the Cisco Catalyst Center" + msg = "Global HTTP write credentials are not present in the Cisco Catalyst Center" self.discovery_specific_cred_failure(msg=msg) if len(http_write_credential_list) > 0: global_credentials_all["httpsWrite"] = [] @@ -820,10 +820,10 @@ def handle_global_credentials(self, response=None): snmp_v2_read_credential_list = global_credentials.get('snmp_v2_read_credential_list') if snmp_v2_read_credential_list: if not isinstance(snmp_v2_read_credential_list, list): - msg = "Global SNMPV2 read credentials must be passed as a list" + msg = "Global SNMPv2 read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if response.get("snmpV2cRead") is None: - msg = "No Global SNMPV2 read credentials are present in the Cisco Catalyst Center" + msg = "Global SNMPv2 read credentials are not resent in the Cisco Catalyst Center" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_read_credential_list) > 0: global_credentials_all["snmpV2cRead"] = [] @@ -837,17 +837,17 @@ def handle_global_credentials(self, response=None): global_credentials_all["snmpV2cRead"].append(snmp.get("id")) global_credentials_all["snmpV2cRead"] = global_credentials_all["snmpV2cRead"][:cred_len] else: - msg = "Kindly ensure you include the description for the Global SNMPV2 Read \ + msg = "Kindly ensure you include the description for the Global SNMPv2 Read \ credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) snmp_v2_write_credential_list = global_credentials.get('snmp_v2_write_credential_list') if snmp_v2_write_credential_list: if not isinstance(snmp_v2_write_credential_list, list): - msg = "Global SNMPV2 write credentials must be passed as a list" + msg = "Global SNMPv2 write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if response.get("snmpV2cWrite") is None: - msg = "No Global SNMPV2 write credentials are present in the Cisco Catalyst Center" + msg = "Global SNMPv2 write credentials are not present in the Cisco Catalyst Center" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_write_credential_list) > 0: global_credentials_all["snmpV2cWrite"] = [] @@ -867,10 +867,10 @@ def handle_global_credentials(self, response=None): snmp_v3_credential_list = global_credentials.get('snmp_v3_credential_list') if snmp_v3_credential_list: if not isinstance(snmp_v3_credential_list, list): - msg = "Global SNMPV3 write credentials must be passed as a list" + msg = "Global SNMPv3 write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if response.get("snmpV3") is None: - msg = "No Global SNMPV3 credentials are present in the Cisco Catalyst Center" + msg = "Global SNMPv3 credentials are not present in the Cisco Catalyst Center" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v3_credential_list) > 0: global_credentials_all["snmpV3"] = [] @@ -884,7 +884,7 @@ def handle_global_credentials(self, response=None): global_credentials_all["snmpV3"].append(snmp.get("id")) global_credentials_all["snmpV3"] = global_credentials_all["snmpV3"][:cred_len] else: - msg = "Kindly ensure you include both the description and the username for the Global SNMPV3 \ + msg = "Kindly ensure you include both the description and the username for the Global SNMPv3 \ to discover the devices" self.discovery_specific_cred_failure(msg=msg) @@ -894,7 +894,7 @@ def handle_global_credentials(self, response=None): msg = "Global net Conf Ports be passed as a list" self.discovery_specific_cred_failure(msg=msg) if response.get("netconfCredential") is None: - msg = "No Global Net Conf Ports are present in the Cisco Catalyst Center" + msg = "Global netconf ports are not present in the Cisco Catalyst Center" self.discovery_specific_cred_failure(msg=msg) if len(net_conf_port_list) > 0: global_credentials_all["netconfCredential"] = [] diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index 189ef45736..d3ac0023d2 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -754,7 +754,7 @@ def handle_global_credentials(self, response=None): msg = "Global CLI credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if response.get("cliCredential") is None: - msg = "No Global CLI credentials are present in the Cisco Catalyst Center" + msg = "Global CLI credentials are not present in the Cisco Catalyst Center" self.discovery_specific_cred_failure(msg=msg) if len(cli_credentials_list) > 0: global_credentials_all["cliCredential"] = [] @@ -777,7 +777,7 @@ def handle_global_credentials(self, response=None): msg = "Global HTTP read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if response.get("httpsRead") is None: - msg = "No Global HTTP read credentials are present in the Cisco Catalyst Center" + msg = "Global HTTP read credentials are not present in the Cisco Catalyst Center" self.discovery_specific_cred_failure(msg=msg) if len(http_read_credential_list) > 0: global_credentials_all["httpsRead"] = [] @@ -800,7 +800,7 @@ def handle_global_credentials(self, response=None): msg = "Global HTTP write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if response.get("httpsWrite") is None: - msg = "No Global HTTP write credentials are present in the Cisco Catalyst Center" + msg = "Global HTTP write credentials are not present in the Cisco Catalyst Center" self.discovery_specific_cred_failure(msg=msg) if len(http_write_credential_list) > 0: global_credentials_all["httpsWrite"] = [] @@ -820,10 +820,10 @@ def handle_global_credentials(self, response=None): snmp_v2_read_credential_list = global_credentials.get('snmp_v2_read_credential_list') if snmp_v2_read_credential_list: if not isinstance(snmp_v2_read_credential_list, list): - msg = "Global SNMPV2 read credentials must be passed as a list" + msg = "Global SNMPv2 read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if response.get("snmpV2cRead") is None: - msg = "No Global SNMPV2 read credentials are present in the Cisco Catalyst Center" + msg = "Global SNMPv2 read credentials are not resent in the Cisco Catalyst Center" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_read_credential_list) > 0: global_credentials_all["snmpV2cRead"] = [] @@ -837,17 +837,17 @@ def handle_global_credentials(self, response=None): global_credentials_all["snmpV2cRead"].append(snmp.get("id")) global_credentials_all["snmpV2cRead"] = global_credentials_all["snmpV2cRead"][:cred_len] else: - msg = "Kindly ensure you include the description for the Global SNMPV2 Read \ + msg = "Kindly ensure you include the description for the Global SNMPv2 Read \ credential to discover the devices" self.discovery_specific_cred_failure(msg=msg) snmp_v2_write_credential_list = global_credentials.get('snmp_v2_write_credential_list') if snmp_v2_write_credential_list: if not isinstance(snmp_v2_write_credential_list, list): - msg = "Global SNMPV2 write credentials must be passed as a list" + msg = "Global SNMPv2 write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if response.get("snmpV2cWrite") is None: - msg = "No Global SNMPV2 write credentials are present in the Cisco Catalyst Center" + msg = "Global SNMPv2 write credentials are not present in the Cisco Catalyst Center" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_write_credential_list) > 0: global_credentials_all["snmpV2cWrite"] = [] @@ -867,10 +867,10 @@ def handle_global_credentials(self, response=None): snmp_v3_credential_list = global_credentials.get('snmp_v3_credential_list') if snmp_v3_credential_list: if not isinstance(snmp_v3_credential_list, list): - msg = "Global SNMPV3 write credentials must be passed as a list" + msg = "Global SNMPv3 write credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if response.get("snmpV3") is None: - msg = "No Global SNMPV3 credentials are present in the Cisco Catalyst Center" + msg = "Global SNMPv3 credentials are not present in the Cisco Catalyst Center" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v3_credential_list) > 0: global_credentials_all["snmpV3"] = [] @@ -884,7 +884,7 @@ def handle_global_credentials(self, response=None): global_credentials_all["snmpV3"].append(snmp.get("id")) global_credentials_all["snmpV3"] = global_credentials_all["snmpV3"][:cred_len] else: - msg = "Kindly ensure you include both the description and the username for the Global SNMPV3 \ + msg = "Kindly ensure you include both the description and the username for the Global SNMPv3 \ to discover the devices" self.discovery_specific_cred_failure(msg=msg) @@ -894,7 +894,7 @@ def handle_global_credentials(self, response=None): msg = "Global net Conf Ports be passed as a list" self.discovery_specific_cred_failure(msg=msg) if response.get("netconfCredential") is None: - msg = "No Global Net Conf Ports are present in the Cisco Catalyst Center" + msg = "Global netconf ports are not present in the Cisco Catalyst Center" self.discovery_specific_cred_failure(msg=msg) if len(net_conf_port_list) > 0: global_credentials_all["netconfCredential"] = [] From 69991071565641a058b803bd5e41fa78066a1018 Mon Sep 17 00:00:00 2001 From: Abinash Date: Tue, 12 Mar 2024 10:28:10 +0000 Subject: [PATCH 07/17] Adding fix for nonetype error in global credentials --- plugins/modules/discovery_intent.py | 2 +- plugins/modules/discovery_workflow_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index f2a5179c40..921859898d 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -826,7 +826,7 @@ def handle_global_credentials(self, response=None): msg = "Global SNMPv2 read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if response.get("snmpV2cRead") is None: - msg = "Global SNMPv2 read credentials are not resent in the Cisco Catalyst Center" + msg = "Global SNMPv2 read credentials are not present in the Cisco Catalyst Center" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_read_credential_list) > 0: global_credentials_all["snmpV2cRead"] = [] diff --git a/plugins/modules/discovery_workflow_manager.py b/plugins/modules/discovery_workflow_manager.py index e9667a4bb1..7557c077d6 100644 --- a/plugins/modules/discovery_workflow_manager.py +++ b/plugins/modules/discovery_workflow_manager.py @@ -826,7 +826,7 @@ def handle_global_credentials(self, response=None): msg = "Global SNMPv2 read credentials must be passed as a list" self.discovery_specific_cred_failure(msg=msg) if response.get("snmpV2cRead") is None: - msg = "Global SNMPv2 read credentials are not resent in the Cisco Catalyst Center" + msg = "Global SNMPv2 read credentials are not present in the Cisco Catalyst Center" self.discovery_specific_cred_failure(msg=msg) if len(snmp_v2_read_credential_list) > 0: global_credentials_all["snmpV2cRead"] = [] From fba3a3a52f5f049753c71d4082d63973cbf46223 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 12 Mar 2024 16:28:18 +0530 Subject: [PATCH 08/17] Revert "add space after comma in playbook" This reverts commit 540d82f822005c6ae5d1b163d089247661f5adac. --- playbooks/inventory_workflow_manager.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playbooks/inventory_workflow_manager.yml b/playbooks/inventory_workflow_manager.yml index dc2dfe7077..11f58ce939 100644 --- a/playbooks/inventory_workflow_manager.yml +++ b/playbooks/inventory_workflow_manager.yml @@ -69,13 +69,13 @@ assign_device_member_to_tag: tag_name: Pre_test_check1 assign_interface_member_to_tag: - interface_names_list: ['GigabitEthernet1/0/1', 'GigabitEthernet1/0/3'] + interface_names_list: ['GigabitEthernet1/0/1','GigabitEthernet1/0/3'] tag_name: Pre_test_check1 remove_device_tag_member: tag_name: March_4th remove_interface_tag_member: - interface_names_list: ['GigabitEthernet1/0/1', 'GigabitEthernet1/0/3'] + interface_names_list: ['GigabitEthernet1/0/1','GigabitEthernet1/0/3'] tag_name: March_4th delete_tag: tag_name: string_device_tag From f97653a61f255d0dc6d9c24c973310a15b4d813d Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 12 Mar 2024 16:29:08 +0530 Subject: [PATCH 09/17] Revert "Write the API for the creation/updation of tag for devices/interfaces with or without dynamic rules, assigning device/interface members to the tag, removing device/interface members from the tag, deleting the tag from Catalyst center, added documentation and examples for the same." This reverts commit 01e1effb954725ffe8d7664909451742ff7aa72c. --- playbooks/inventory_workflow_manager.yml | 40 - plugins/modules/inventory_workflow_manager.py | 1208 +---------------- 2 files changed, 57 insertions(+), 1191 deletions(-) diff --git a/playbooks/inventory_workflow_manager.yml b/playbooks/inventory_workflow_manager.yml index 11f58ce939..6a54a24df3 100644 --- a/playbooks/inventory_workflow_manager.yml +++ b/playbooks/inventory_workflow_manager.yml @@ -40,46 +40,6 @@ type: "{{item.type}}" device_resync: "{{item.device_resync}}" reboot_device: "{{item.reboot_device}}" - create_tag: - tag_name: Pre_test_check1 - systemTag: False - description: "Add description while creating the tag" - dynamic_rules: - - member_type: "networkdevice" - rules: - operation: "AND" - items: - - operation: "OR" - items: - - operation: "ILIKE" - name: "hostname" - value: "%switch%" - - operation: "ILIKE" - name: "hostname" - value: "%Router%" - - operation: ILIKE - name: family - value: "%Switch%" - - update_tag: - tag_name: Pre_test_check1 - system_tag: False - description: "Add description while updating the tag" - - assign_device_member_to_tag: - tag_name: Pre_test_check1 - assign_interface_member_to_tag: - interface_names_list: ['GigabitEthernet1/0/1','GigabitEthernet1/0/3'] - tag_name: Pre_test_check1 - - remove_device_tag_member: - tag_name: March_4th - remove_interface_tag_member: - interface_names_list: ['GigabitEthernet1/0/1','GigabitEthernet1/0/3'] - tag_name: March_4th - delete_tag: - tag_name: string_device_tag - update_device_role: role: "{{item.role}}" add_user_defined_field: diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index 2e4c7379ad..9c8e01a24b 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -356,135 +356,6 @@ process. If unspecified, the system will check the device status every 2 seconds by default. type: int default: 2 - create_tag: - description: This parameter used to collect all the details needed to create the tag. - type: dict - suboptions: - system_tag: - description: It's a unique identifier associated with each tag in Cisco Catalyst Center, used internally by the system - for management and reference purposes. Specifies whether the tag is a system tag or not. - type: bool - description: - description: Provides a description for the tag, explaining its purpose or usage. - type: str - dynamic_rules: - description: Defines dynamic rules associated with the tag, which specify conditions for membership. - type: list - elements: dict - suboptions: - member_type: - description: Specifies the type of member (networkdevice, interface) targeted by the dynamic rule. - - 'networkdevice' - Indicates that the dynamic rule applies to network devices. - - 'interface' - Indicates that the dynamic rule applies to interfaces. - type: str - rules: - description: Contains the conditions/rules for membership. - type: dict - suboptions: - items: - description: Represents the items for membership. - type: list - elements: dict - suboptions: - operation: - description: Specifies the operation used to evaluate the condition (equals, not equals, etc.). - type: str - name: - description: Specifies the name of the attribute to evaluate for membership. - type: str - value: - description: Specifies the value to compare with the attribute for membership. - type: str - tag_name: - description: Specifies the name of the tag. It's required while creating the tag. - type: str - update_tag: - description: This parameter used to collect all the details needed to update the tag. - type: dict - suboptions: - system_tag: - description: It's a unique identifier associated with each tag in Cisco Catalyst Center, used internally by the system - for management and reference purposes. Specifies whether the tag is a system tag or not. - type: bool - description: - description: Provides a description for the tag, explaining its purpose or usage. - type: str - dynamic_rules: - description: Defines dynamic rules associated with the tag, which specify conditions for membership. - type: list - elements: dict - suboptions: - member_type: - description: Specifies the type of member (networkdevice, interface) targeted by the dynamic rule. - - 'networkdevice' - Indicates that the dynamic rule applies to network devices. - - 'interface' - Indicates that the dynamic rule applies to interfaces. - type: str - rules: - description: Contains the conditions/rules for membership. - type: dict - suboptions: - items: - description: Represents the items for membership. - type: list - elements: dict - suboptions: - operation: - description: Specifies the operation used to evaluate the condition (equals, not equals, etc.). - type: str - name: - description: Specifies the name of the attribute to evaluate for membership. - type: str - value: - description: Specifies the value to compare with the attribute for membership. - type: str - name: - description: Specifies the name of the tag. It's required while creating the tag. - type: str - assign_device_member_to_tag: - description: Assigning the device members to the tag. - type: dict - suboptions: - tag_name: - description: Name of the tag which is assigned to the device members. - type: str - assign_interface_member_to_tag: - description: Assigning the interface members to the tag. - type: dict - suboptions: - tag_name: - description: Name of the tag which is assigned to the device members - type: str - interface_names_list: - description: Specify the list of interface names to to which the given tag will be removed. - (For example, GigabitEthernet1/0/11, FortyGigabitEthernet1/1/2) . - type: list - elements: str - remove_device_tag_member: - description: Removing the device members from the tag. - type: dict - suboptions: - tag_name: - description: Name of the tag which is to be removed from the device member. - type: str - remove_interface_tag_member: - description: Removing the interface members from the tag. - type: dict - suboptions: - tag_name: - description: Name of the tag which is to be removed from the interface member. - type: str - interface_names_list: - description: Specify the list of interface names to to which the given tag will be removed. - (For example, GigabitEthernet1/0/11, FortyGigabitEthernet1/1/2) . - type: list - elements: str - delete_tag: - description: Deleting the tag from the inventory. - type: dict - suboptions: - tag_name: - description: Name of the tag which is to be deleted. - type: str requirements: - dnacentersdk >= 2.5.5 @@ -808,107 +679,6 @@ interface_name: ["GigabitEthernet1/0/11", FortyGigabitEthernet1/1/1] clear_mac_address_table: True -- name: Create tag in Inventory for device/interface without dynamic rules. - cisco.dnac.inventory_workflow_manager: - dnac_host: "{{dnac_host}}" - dnac_username: "{{dnac_username}}" - dnac_password: "{{dnac_password}}" - dnac_verify: "{{dnac_verify}}" - dnac_port: "{{dnac_port}}" - dnac_version: "{{dnac_version}}" - dnac_debug: "{{dnac_debug}}" - dnac_log_level: "{{dnac_log_level}}" - dnac_log: False - state: merged - config: - - create_tag: - tag_name: Test_tag - description: "Give the description of the tag" - system_tag: false - -- name: Create tag in Inventory for device/interface with dynamic rules. - cisco.dnac.inventory_workflow_manager: - dnac_host: "{{dnac_host}}" - dnac_username: "{{dnac_username}}" - dnac_password: "{{dnac_password}}" - dnac_verify: "{{dnac_verify}}" - dnac_port: "{{dnac_port}}" - dnac_version: "{{dnac_version}}" - dnac_debug: "{{dnac_debug}}" - dnac_log_level: "{{dnac_log_level}}" - dnac_log: False - state: merged - config: - - create_tag: - tag_name: Test_tag123 - system_tag: false - dynamic_rules: - - member_type: "networkdevice" - rules: - operation: "AND" - items: - - operation: "OR" - items: - - operation: "ILIKE" - name: "hostname" - value: "%switch%" - - operation: "ILIKE" - name: "hostname" - value: "%Router%" - -- name: Update the tag in Inventory for device/interface without dynamic rules. - cisco.dnac.inventory_workflow_manager: - dnac_host: "{{dnac_host}}" - dnac_username: "{{dnac_username}}" - dnac_password: "{{dnac_password}}" - dnac_verify: "{{dnac_verify}}" - dnac_port: "{{dnac_port}}" - dnac_version: "{{dnac_version}}" - dnac_debug: "{{dnac_debug}}" - dnac_log_level: "{{dnac_log_level}}" - dnac_log: False - state: merged - config: - - update_tag: - tag_name: Test_tag - description: "Give the description of the tag" - system_tag: False - -- name: Assigning device memebers to the Tag in Inventory. - cisco.dnac.inventory_workflow_manager: - dnac_host: "{{dnac_host}}" - dnac_username: "{{dnac_username}}" - dnac_password: "{{dnac_password}}" - dnac_verify: "{{dnac_verify}}" - dnac_port: "{{dnac_port}}" - dnac_version: "{{dnac_version}}" - dnac_debug: "{{dnac_debug}}" - dnac_log_level: "{{dnac_log_level}}" - dnac_log: False - state: merged - config: - - ip_address_list: ["204.1.2.1", "204.1.2.2"] - assign_device_member_to_tag: - tag_name: Test_tag - -- name: Assigning interface memebers to the Tag in Inventory. - cisco.dnac.inventory_workflow_manager: - dnac_host: "{{dnac_host}}" - dnac_username: "{{dnac_username}}" - dnac_password: "{{dnac_password}}" - dnac_verify: "{{dnac_verify}}" - dnac_port: "{{dnac_port}}" - dnac_version: "{{dnac_version}}" - dnac_debug: "{{dnac_debug}}" - dnac_log_level: "{{dnac_log_level}}" - dnac_log: False - state: merged - config: - - ip_address_list: ["204.1.2.1", "204.1.2.2"] - assign_interface_member_to_tag: - interface_name_list: ['GigabitEthernet1/0/1','GigabitEthernet1/0/3'] - tag_name: Test_tag - - name: Export Device Details in a CSV file Interface details with IP Address cisco.dnac.inventory_workflow_manager: dnac_host: "{{dnac_host}}" @@ -1016,57 +786,6 @@ add_user_defined_field: name: "Test123" -- name: Removing tag from the device memebers in Inventory. - cisco.dnac.inventory_workflow_manager: - dnac_host: "{{dnac_host}}" - dnac_username: "{{dnac_username}}" - dnac_password: "{{dnac_password}}" - dnac_verify: "{{dnac_verify}}" - dnac_port: "{{dnac_port}}" - dnac_version: "{{dnac_version}}" - dnac_debug: "{{dnac_debug}}" - dnac_log_level: "{{dnac_log_level}}" - dnac_log: False - state: merged - config: - - ip_address_list: ["204.1.2.1", "204.1.2.2"] - remove_device_tag_member: - tag_name: Test_tag - -- name: Removing tag from the interface memebers in Inventory. - cisco.dnac.inventory_workflow_manager: - dnac_host: "{{dnac_host}}" - dnac_username: "{{dnac_username}}" - dnac_password: "{{dnac_password}}" - dnac_verify: "{{dnac_verify}}" - dnac_port: "{{dnac_port}}" - dnac_version: "{{dnac_version}}" - dnac_debug: "{{dnac_debug}}" - dnac_log_level: "{{dnac_log_level}}" - dnac_log: False - state: merged - config: - - ip_address_list: ["204.1.2.1", "204.1.2.2"] - remove_interface_tag_member: - interface_name_list: ['GigabitEthernet1/0/1','GigabitEthernet1/0/3'] - tag_name: Test_tag - -- name: Deleting tag from the Inventory. - cisco.dnac.inventory_workflow_manager: - dnac_host: "{{dnac_host}}" - dnac_username: "{{dnac_username}}" - dnac_password: "{{dnac_password}}" - dnac_verify: "{{dnac_verify}}" - dnac_port: "{{dnac_port}}" - dnac_version: "{{dnac_version}}" - dnac_debug: "{{dnac_debug}}" - dnac_log_level: "{{dnac_log_level}}" - dnac_log: False - state: merged - config: - - delete_tag: - tag_name: Test_tag - """ RETURN = r""" @@ -1213,66 +932,6 @@ def validate_input(self): }, 'resync_retry_count': {'default': 200, 'type': 'int'}, 'resync_retry_interval': {'default': 2, 'type': 'int'}, - }, - 'create_tag': { - 'type': 'dict', - 'tag_name': {'type': 'str'}, - 'system_tag': {'type': 'str'}, - 'description': {'type': 'str'}, - 'dynamic_rules': { - 'type': 'list', - 'member_type': {'type': 'str'}, - 'rules': { - 'type': 'dict', - 'items': { - 'type': 'list', - 'operation': {'type': 'str'}, - 'name': {'type': 'str'}, - 'value': {'type': 'str'}, - }, - }, - }, - }, - 'update_tag': { - 'type': 'dict', - 'tag_name': {'type': 'str'}, - 'system_tag': {'type': 'str'}, - 'description': {'type': 'str'}, - 'dynamic_rules': { - 'type': 'list', - 'member_type': {'type': 'str'}, - 'rules': { - 'type': 'dict', - 'items': { - 'type': 'list', - 'operation': {'type': 'str'}, - 'name': {'type': 'str'}, - 'value': {'type': 'str'}, - }, - }, - }, - }, - 'assign_device_member_to_tag': { - 'type': 'dict', - 'tag_name': {'type': 'str'}, - }, - 'assign_interface_member_to_tag': { - 'type': 'dict', - 'tag_name': {'type': 'str'}, - 'interface_names_list': {'type': 'str'}, - }, - 'remove_device_tag_member': { - 'type': 'dict', - 'tag_name': {'type': 'str'}, - }, - 'remove_interface_tag_member': { - 'type': 'dict', - 'tag_name': {'type': 'str'}, - 'interface_names_list': {'type': 'list', 'elements': 'str'}, - }, - 'delete_tag': { - 'type': 'dict', - 'tag_name': {'type': 'str'}, } } @@ -1297,7 +956,7 @@ def validate_input(self): def get_device_ips_from_config_priority(self): """ Retrieve device IPs based on the configuration. - Parameters: + Args: - self (object): An instance of a class used for interacting with Cisco Cisco Catalyst Center. Returns: list: A list containing device IPs. @@ -3097,7 +2756,7 @@ def clear_mac_address(self, interface_id, deploy_mode, interface_name): def update_interface_detail_of_device(self, device_to_update): """ Update interface details for a device in Cisco Catalyst Center. - Parameters: + Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_to_update (list): A list of IP addresses of devices to be updated. Returns: @@ -3273,775 +2932,88 @@ def check_device_update_execution_response(self, response, device_ip): return self - def check_and_get_tag_id(self, name): + def get_want(self, config): """ - Check for a tag by name and retrieve its ID. + Get all the device related information from playbook that is needed to be + add/update/delete/resync device in Cisco Catalyst Center. Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - name (str): The name of the tag to check and retrieve its ID. + config (dict): A dictionary containing device-related information from the playbook. Returns: - str or None: The ID of the tag if found, or None if the tag does not exist or an error occurs. + dict: A dictionary containing the extracted device parameters and other relevant information. Description: - This function checks for the existence of a tag in Cisco Catalyst Center by its name and retrieves its ID. - It queries the Cisco Catalyst Center API using the 'get_tag' function to fetch tag details based on the provided name. - If the tag is found, its ID is extracted from the API response. - If the tag is not found or an error occurs during the process, None is returned. + Retrieve all the device-related information from the playbook needed for adding, updating, deleting, + or resyncing devices in Cisco Catalyst Center. """ - try: - tag_id = None - response = self.dnac._exec( - family="tag", - function='get_tag', - params={"name": name}, - ) - self.log("Received API response from 'get_tag': {0}".format(str(response)), "DEBUG") - response = response.get('response') - - if not response: - self.log("Did not receive any response for the tag '{0}' so cannot get the id.".format(name), "INFO") - return tag_id - - tag_id = response[0].get('id') + want = {} + device_params = self.get_device_params(config) + want["device_params"] = device_params - except Exception as e: - error_msg = "An exception occurred while getting the tag '{0}' details, due to - {1}".format(name, str(e)) - self.log(error_msg, "ERROR") - self.result['changed'] = False - self.result['response'] = error_msg + self.want = want + self.msg = "Successfully collected all parameters from the playbook " + self.status = "success" + self.log("Desired State (want): {0}".format(str(self.want)), "INFO") - return tag_id + return self - def create_tag(self, tag_params): + def get_diff_merged(self, config): """ - Create a tag in Cisco Catalyst Center based on the provided parameters. + Merge and process differences between existing devices and desired device configuration in Cisco Catalyst Center. Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - tag_params (dict): A dictionary containing parameters for creating the tag.It should have the following keys: - - 'tag_name' (str): The name of the tag to be created. - - 'system_tag' (bool): A boolean indicating whether the tag is a system tag (optional, default is False). - - 'dynamic_rules' (list): A list of dictionaries representing dynamic rules for the tag (optional). - Each dictionary in the 'dynamic_rules' list should contain the following keys: - - 'member_type' (str): The type of member for the dynamic rule (optional, default is 'networkdevice'). - - 'rules' (dict): A dictionary containing rules for the dynamic rule. + config (dict): A dictionary containing the desired device configuration and relevant information from the playbook. Returns: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. + object: An instance of the class with updated results and status based on the processing of differences. Description: - This function creates a tag in Cisco Catalyst Center based on the provided parameters. - It constructs the payload for the tag creation API call using the provided parameters. - If dynamic rules are provided, they are formatted and included in the payload. - The function then checks if a tag with the same name already exists. - If a tag with the same name exists, the function returns without creating the tag. - Otherwise, it calls the create tag API and monitors the task execution status. + The function processes the differences and, depending on the changes required, it may add, update, + or resynchronize devices in Cisco Catalyst Center. + The updated results and status are stored in the class instance for further use. """ + devices_to_add = self.have["device_not_in_ccc"] + device_type = self.config[0].get("type", "NETWORK_DEVICE") + device_resynced = self.config[0].get("device_resync", False) + device_updated = self.config[0].get("device_updated", False) + device_reboot = self.config[0].get("reboot_device", False) + credential_update = self.config[0].get("credential_update", False) - tag_payload = {} - dynamicRules = [] - dynamic_rules = tag_params.get('dynamic_rules') - - if not dynamic_rules: - self.log("There is no dynamic roles while creating tag for the device or interface", "INFO") - else: - for rule in dynamic_rules: - dynamic_rule_dict = {} - dynamic_rule_dict['memberType'] = rule.get('member_type', "networkdevice") - dynamic_rule_dict['rules'] = rule.get('rules') - dynamicRules.append(dynamic_rule_dict) - - system_tag = tag_params.get('system_tag', False) - name = tag_params.get('tag_name') - - tag_payload['name'] = name - tag_payload['systemTag'] = system_tag - if dynamicRules: - tag_payload['dynamicRules'] = dynamicRules + config['type'] = device_type + if device_type == "FIREPOWER_MANAGEMENT_SYSTEM": + config['http_port'] = self.config[0].get("http_port", "443") - # Check if tag with given name is already created then we have to return from here only - tag_id = self.check_and_get_tag_id(name) + config['ip_address_list'] = devices_to_add - if tag_id: - self.status = "success" - self.msg = "Cannot create the tag, a group already exists with the same name '{0}'".format(name) + if not config['ip_address_list']: + self.msg = "Devices '{0}' already present in Cisco Catalyst Center".format(self.have['devices_in_playbook']) self.log(self.msg, "INFO") - self.result['response'] = self.msg self.result['changed'] = False - return self - - try: - # Now need to call the create tag API - response = self.dnac._exec( - family="tag", - function='create_tag', - op_modifies=True, - params=tag_payload, - ) - self.log("Received API response from 'create_tag': {0}".format(str(response)), "DEBUG") - - if not (response and isinstance(response, dict)): - self.status = "failed" - self.msg = "Received an empty response from the create tag API which indicates create tag API not executed successfully." - self.result['response'] = self.msg - return self + self.result['response'] = self.msg + else: + # To add the devices in inventory + input_params = self.want.get("device_params") + device_params = input_params.copy() - task_id = response.get('response').get('taskId') + if not device_params['snmpVersion']: + device_params['snmpVersion'] = "v3" + device_params['ipAddress'] = config['ip_address_list'] - while True: - execution_details = self.get_task_details(task_id) + if device_params['snmpVersion'] == "v2": + params_to_remove = ["snmpAuthPassphrase", "snmpAuthProtocol", "snmpMode", "snmpPrivPassphrase", "snmpPrivProtocol", "snmpUserName"] + for param in params_to_remove: + device_params.pop(param, None) - if execution_details.get("isError"): + if not device_params['snmpROCommunity']: self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Failed to create the tag '{0}' for the device/interface due to {1}".format(name, failure_reason) - else: - self.msg = "Failed to clear the create the tag '{0}' for the device/interface".format(name) + self.msg = "Required parameter 'snmpROCommunity' for adding device with snmmp version v2 is not present" + self.result['msg'] = self.msg self.log(self.msg, "ERROR") - self.result['response'] = self.msg - break - elif 'created successfully' in execution_details.get("progress"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - self.msg = "Successfully created the tag '{0}' for the device/interface.".format(name) - self.log(self.msg, "INFO") - break - - except Exception as e: - self.status = "failed" - self.msg = "An exception occurred while creating the tag '{0}' for the device/interface due to - {1}".format(name, str(e)) - self.log(self.msg, "ERROR") - self.result['response'] = self.msg + return self + else: + if not device_params['snmpMode']: + device_params['snmpMode'] = "AUTHPRIV" - return self - - def update_tag(self, tag_params): - """ - Update a tag in Cisco Catalyst Center based on the provided parameters. - Parameters: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - tag_params (dict): A dictionary containing parameters for updating the tag. - It should have the following keys: - - 'tag_name' (str): The name of the tag to be updated. - - 'system_tag' (bool): A boolean indicating whether the tag is a system tag (optional). - - 'description' (str): A description for the tag (optional). - - 'dynamic_rules' (list): A list of dictionaries representing dynamic rules for the tag (optional). - Each dictionary in the 'dynamic_rules' list should contain the following keys: - - 'member_type' (str): The type of member for the dynamic rule (optional, default is 'networkdevice'). - - 'rules' (dict): A dictionary containing rules for the dynamic rule. - Returns: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - Description: - This function updates a tag in Cisco Catalyst Center based on the provided parameters. - It first checks if the tag exists by calling the 'check_and_get_tag_id' method. - If the tag does not exist, the function sets the status to 'failed' and returns. - If the tag exists, the function constructs the payload for the tag update API call using the provided parameters. - If dynamic rules are provided, they are formatted and included in the payload. - The function then calls the update tag API and monitors the task execution status. - """ - - # Get the tag details from Cisco Catalyst Center - name = tag_params.get('tag_name') - tag_id = self.check_and_get_tag_id(name) - - if not tag_id: - self.status = "failed" - self.msg = "Cannot update the tag '{0}' as group is not present in Cisco Catalyst Center.".format(name) - self.log(self.msg, "INFO") - self.result['response'] = self.msg - return self - - response = self.dnac._exec( - family="tag", - function='get_tag', - op_modifies=True, - params={"name": name} - ) - self.log("Received API response from 'get_tag': {0}".format(str(response)), "DEBUG") - response = response.get('response')[0] - - tag_payload = {} - dynamicRules = [] - dynamic_rules = tag_params.get('dynamic_rules') - - if not dynamic_rules: - dynamic_rules = response.get(dynamic_rules) - if not dynamic_rules: - self.log("There is no dynamic roles while updating tag for the device or interface", "INFO") - else: - dynamicRules.append(dynamic_rules) - else: - for rule in dynamic_rules: - dynamic_rule_dict = {} - dynamic_rule_dict['memberType'] = rule.get('member_type', "networkdevice") - dynamic_rule_dict['rules'] = rule.get('rules') - dynamicRules.append(dynamic_rule_dict) - - system_tag = tag_params.get('system_tag') - description = tag_params.get('description') - - if not system_tag: - system_tag = response.get('system_tag') - if not description: - description = response.get('description', "") - - tag_payload['name'] = name - tag_payload['systemTag'] = system_tag - tag_payload['id'] = tag_id - tag_payload['description'] = description - - if dynamicRules: - tag_payload['dynamicRules'] = dynamicRules - - try: - # Now need to call the update tag API - response = self.dnac._exec( - family="tag", - function='update_tag', - op_modifies=True, - params=tag_payload, - ) - self.log("Received API response from 'update_tag': {0}".format(str(response)), "DEBUG") - - if not (response and isinstance(response, dict)): - self.status = "failed" - self.msg = """Received an empty response from the updating the tag '{0}' which indicates tag updation - not executed successfully.""".format(name) - self.result['response'] = self.msg - return self - - response = response.get('response') - task_id = response.get('taskId') - - while True: - execution_details = self.get_task_details(task_id) - - if execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Failed to update the tag '{0}' for the device/interface due to {1}".format(name, failure_reason) - else: - self.msg = "Failed to clear the update the tag '{0}' for the device/interface".format(name) - self.log(self.msg, "ERROR") - self.result['response'] = self.msg - break - elif 'updated successfully' in execution_details.get("progress"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - self.msg = "Successfully updated the tag '{0}' for the device/interface.".format(name) - self.log(self.msg, "INFO") - break - - except Exception as e: - self.status = "failed" - self.msg = "An exception occurred while updating the tag '{0}' for the device/interface due to - {1}".format(name, str(e)) - self.log(self.msg, "WARNING") - self.result['changed'] = False - self.result['response'] = self.msg - - return self - - def add_member_to_tag(self, member_payload, tag_name): - - try: - # Now need to call the create tag API - response = self.dnac._exec( - family="tag", - function='add_members_to_the_tag', - op_modifies=True, - params=member_payload, - ) - self.log("Received API response from 'add_members_to_the_tag': {0}".format(str(response)), "DEBUG") - - if not (response and isinstance(response, dict)): - self.status = "failed" - self.msg = """Received an empty response from the Add member to tag API for the tag '{0}' which - indicates API not executed successfully""".format(tag_name) - self.result['response'] = self.msg - return self - - task_id = response.get('response').get('taskId') - - while True: - execution_details = self.get_task_details(task_id) - - if execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Failed to assign the tag '{0}' to the member due to {1}".format(tag_name, failure_reason) - else: - self.msg = "Failed to assign the tag '{0}' to the member".format(tag_name) - self.log(self.msg, "ERROR") - self.result['response'] = self.msg - break - elif 'Successfully added' in execution_details.get("progress"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - self.msg = "Successfully executed the task of assigning the tag '{0}' to the member(s) ".format(tag_name) - self.log(self.msg, "INFO") - break - - except Exception as e: - self.status = "failed" - self.msg = "An exception occurred while assigning the tag '{0}' to the member(s) due to - {1}".format(tag_name, str(e)) - self.log(self.msg, "ERROR") - self.result['changed'] = False - self.result['response'] = self.msg - - return self - - def assign_device_member_to_tag(self): - """ - Add member(s) to a tag in Cisco Catalyst Center based on the provided payload. - Parameters: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - member_payload (dict): A dictionary containing the payload for adding member(s) to the tag. - tag_name (str): The name of the tag to which member(s) will be added. - Returns: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - Description: - This function adds member(s) to a tag in Cisco Catalyst Center based on the provided payload. - It constructs the payload for the API call using the provided member payload and tag name. - The function then calls the add members to tag API and monitors the task execution status. - """ - - device_member_params = self.config[0].get('assign_device_member_to_tag') - device_ips = self.get_device_ips_from_config_priority() - device_ids = self.get_device_ids(device_ips) - tag_name = device_member_params.get('tag_name') - - if not tag_name: - self.status = "failed" - self.msg = "Required parameter 'tag_name' is missing for adding device member to the tag." - self.log(self.msg, "ERROR") - self.result['response'] = self.msg - return self - - tag_id = self.check_and_get_tag_id(tag_name) - - if not tag_id: - # create the device tag in Cisco Catalyst Center with the given tag name - self.log("The given device tag '{0}' is not created in Cisco Catalyst Center".format(tag_name), "INFO") - tag_params = { - 'name': tag_name - } - self.create_device_tag(tag_params) - - # Since the tag is created so we will fetch the tag_id in order to assign member to tag - tag_id = self.check_and_get_tag_id(tag_name) - - mem_payload = { - 'networkdevice': device_ids - } - - device_member_payload = { - 'id': tag_id, - 'payload': mem_payload - } - - self.add_member_to_tag(device_member_payload, tag_name).check_return_status() - - return self - - def assign_interface_member_to_tag(self): - """ - Assign interface member(s) to a tag in Cisco Catalyst Center based on configuration parameters. - Parameters: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - Returns: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - Description: - This function assigns interface member(s) to a tag in Cisco Catalyst Center based on configuration parameters. - It retrieves the configuration parameters including tag name, device IPs, and interface names list. - If the tag name or interface names list is missing, the function sets the status to 'failed' and returns. - It then checks if the tag exists in Cisco Catalyst Center, and if not, creates the tag. - Next, it retrieves device IDs corresponding to the provided device IPs. - For each device, it retrieves the interface ID(s) corresponding to the provided interface names. - If interface IDs are found, it constructs the payload for adding interface member(s) to the tag. - The function calls the add member to tag API and monitors the task execution status. - """ - - interface_member_params = self.config[0].get('assign_interface_member_to_tag') - device_ips = self.get_device_ips_from_config_priority() - device_ids = self.get_device_ids(device_ips) - tag_name = interface_member_params.get('tag_name') - interface_names_list = interface_member_params.get('interface_names_list') - - if not tag_name or not interface_names_list: - self.status = "failed" - self.msg = "Required parameter 'tag_name/interface_name' is missing for adding device member to the tag." - self.log(self.msg, "ERROR") - self.result['response'] = self.msg - return self - - tag_id = self.check_and_get_tag_id(tag_name) - if not tag_id: - # create the tag in Cisco Catalyst Center with the given tag name - self.log("The given tag '{0}' is not created in Cisco Catalyst Center".format(tag_name), "INFO") - tag_params = { - 'name': tag_name - } - self.create_device_tag(tag_params) - - # Since the tag is created so we will fetch the tag_id in order to assign member to tag - tag_id = self.check_and_get_tag_id(tag_name) - - interface_ids = [] - for device_id in device_ids: - for interface_name in interface_names_list: - interface_id = self.get_interface_from_id_and_name(device_id, interface_name) - if interface_id: - interface_ids.append(interface_id) - - if not interface_ids: - self.status = "failed" - self.msg = "Doesnot receive any interface ids so cannot perfrom assigning member to tag operation" - self.log(self.msg, "ERROR") - self.result['response'] = self.msg - return self - - mem_payload = { - 'interface': interface_ids - } - - interface_member_payload = { - 'id': tag_id, - 'payload': mem_payload - } - self.add_member_to_tag(interface_member_payload, tag_name).check_return_status() - - return self - - def remove_tag_member(self, remove_tag_member_payload, tag_name): - """ - Remove tag from member(s) in Cisco Catalyst Center based on the provided payload. - Parameters: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - remove_tag_member_payload (dict): A dictionary containing the payload for removing tag from member(s). - tag_name (str): The name of the tag to be removed from the member(s). - Returns: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - Description: - This function removes tag from member(s) in Cisco Catalyst Center based on the provided payload. - It constructs the payload for the API call using the provided remove tag member payload and tag name. - The function then calls the remove tag member API and monitors the task execution status. - """ - - try: - response = self.dnac._exec( - family="tag", - function='remove_tag_member', - params=remove_tag_member_payload, - ) - - self.log("Received API response from 'remove_tag_member': {0}".format(str(response)), "DEBUG") - - response = response.get('response') - if not response: - self.status = "failed" - self.log("Did not receive any response for the tag '{0}' so cannot remove tag from member.".format(tag_name), "INFO") - return self - - task_id = response.get('taskId') - - while True: - execution_details = self.get_task_details(task_id) - - if execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Failed to remove the tag '{0}' from the member due to {1}".format(tag_name, failure_reason) - else: - self.msg = "Failed to remove the tag '{0}' from the member".format(tag_name) - self.log(self.msg, "ERROR") - self.result['response'] = self.msg - break - elif 'deleted successfully' in execution_details.get("progress"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - self.msg = "Successfully executed the task of removing the tag '{0}' to the member(s) ".format(tag_name) - self.log(self.msg, "INFO") - break - - except Exception as e: - self.status = "failed" - self.msg = "An exception occurred while removing the tag '{0}' from the member, due to - {1}".format(tag_name, str(e)) - self.log(self.msg, "ERROR") - self.result['changed'] = False - self.result['response'] = self.msg - - return self - - def remove_device_tag_member(self, tag_name): - """ - Remove device member(s) from a tag in Cisco Catalyst Center based on the provided tag name. - Parameters: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - tag_name (str): The name of the tag from which device member(s) will be removed. - Returns: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - Description: - This function removes device member(s) from a tag in Cisco Catalyst Center based on the provided tag name. - It retrieves the tag ID corresponding to the given tag name, if not found, the function sets the status to - 'failed' and returns. Then retrieves the device IPs from the configuration.For each device IP, it retrieves - the device ID and constructs the payload for removing device member(s) from the tag. - The function calls the remove tag member API and monitors the task execution status. - """ - - try: - # Get the tag id with the given tag name - tag_id = self.check_and_get_tag_id(tag_name) - - if not tag_id: - self.msg = """Given tag '{0}' not present in Cisco Catalyst Center so cannot perform the remove tag from - the member operation""".format(tag_name) - self.log(self.msg, "INFO") - self.result['response'] = self.msg - return self - - member_ips = self.get_device_ips_from_config_priority() - - for member_ip in member_ips: - member_id = self.get_device_ids([member_ip]) - remove_tag_member_payload = { - 'member_id': member_id[0], - 'id': tag_id - } - self.remove_tag_member(remove_tag_member_payload, tag_name).check_return_status() - self.log("Tag '{0}' successfully removed from the member {1} in Cisco Catalyst Center".format(tag_name, member_ip), "INFO") - - except Exception as e: - self.status = "failed" - self.msg = "An exception occurred while removing the member from the tag '{0}' details, due to - {1}".format(tag_name, str(e)) - self.log(self.msg, "ERROR") - self.result['changed'] = False - self.result['response'] = self.msg - - return self - - def remove_interface_tag_member(self, tag_name, interface_name_list): - """ - Remove interface member(s) from a tag in Cisco Catalyst Center based on the provided tag name and interface names. - Parameters: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - tag_name (str): The name of the tag from which interface member(s) will be removed. - interface_name_list (list of str): A list containing the names of interfaces to be removed from the tag. - Returns: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - Description: - This function removes interface member(s) from a tag in Cisco Catalyst Center based on the provided tag name and interface names. - It retrieves the tag ID corresponding to the given tag name and if not found, the function sets the status to 'failed' and - returns. Then retrieves the device IPs from the configuration and gets their corresponding device IDs. - For each device ID and interface name combination, it retrieves the interface ID and constructs the payload for removing - interface member(s) from the tag. The function calls the remove tag member API and monitors the task execution status. - """ - - try: - # Get the tag id with the given tag name - tag_id = self.check_and_get_tag_id(tag_name) - - if not tag_id: - self.msg = """Given tag '{0}' not present in Cisco Catalyst Center so cannot perform the remove tag from - the member operation""".format(tag_name) - self.log(self.msg, "INFO") - self.result['response'] = self.msg - return self - - device_ips = self.get_device_ips_from_config_priority() - device_ids = self.get_device_ids(device_ips) - interface_ids = [] - for device_id in device_ids: - for interface_name in interface_name_list: - interface_id = self.get_interface_from_id_and_name(device_id, interface_name) - if interface_id: - interface_ids.append(interface_id) - - if not interface_ids: - self.status = "failed" - self.msg = "Doesnot receive any interface ids so cannot perfrom remove tag from interface member task." - self.log(self.msg, "ERROR") - self.result['response'] = self.msg - return self - - for interface_id in interface_ids: - remove_tag_member_payload = { - 'member_id': interface_id, - 'id': tag_id - } - self.remove_tag_member(remove_tag_member_payload, tag_name).check_return_status() - self.log("Tag '{0}' successfully removed from the member in Cisco Catalyst Center".format(tag_name), "INFO") - - except Exception as e: - self.status = "failed" - self.msg = "An exception occurred while removing the member from the tag '{0}' details, due to - {1}".format(tag_name, str(e)) - self.log(self.msg, "ERROR") - self.result['changed'] = False - self.result['response'] = self.msg - - return self - - def delete_tag(self, tag_name): - """ - Delete a tag from Cisco Catalyst Center based on the provided tag name. - Parameters: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - tag_name (str): The name of the tag to be deleted. - Returns: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - Description: - This function deletes a tag from Cisco Catalyst Center based on the provided tag name. - It retrieves the tag ID corresponding to the given tag name and if not found, the function - sets the status to 'success' and logs an informational message indicating that the tag is already deleted. - It then constructs the payload for deleting the tag and calls the delete tag API. - The function monitors the task execution status and logs appropriate messages based on the outcome. - """ - try: - # Get the tag id with the given tag name - tag_id = self.check_and_get_tag_id(tag_name) - if not tag_id: - self.status = "success" - self.msg = "Tag '{0}' already deleted from Cisco Catalyst Center".format(tag_name) - self.log(self.msg, "INFO") - self.result['changed'] = False - self.result['response'] = self.msg - return self - - delete_tag_params = { - "id": tag_id - } - - response = self.dnac._exec( - family="tag", - function='delete_tag', - params=delete_tag_params, - ) - - self.log("Received API response from 'delete_tag': {0}".format(str(response)), "DEBUG") - response = response.get('response') - if not response: - self.status = "failed" - self.log("Did not receive any response for the tag '{0}' so cannot delete tag from Cisco Catalyst Center.".format(tag_name), "INFO") - return self - - task_id = response.get('taskId') - - while True: - execution_details = self.get_task_details(task_id) - - if execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Failed to delete the tag '{0}' from Cisco Catalyst Center due to {1}".format(tag_name, failure_reason) - else: - self.msg = "Failed to delete the tag '{0}' from Cisco Catalyst Center".format(tag_name) - self.log(self.msg, "ERROR") - self.result['response'] = self.msg - break - elif 'deleted successfully' in execution_details.get("progress"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - self.msg = "Successfully executed the task of deleting the tag '{0}' Cisco Catalyst Center.".format(tag_name) - self.log(self.msg, "INFO") - break - - except Exception as e: - self.status = "failed" - self.msg = "An exception occurred while deleting the tag '{0}' from Cisco Catalyst Center, due to - {1}".format(tag_name, str(e)) - self.log(self.msg, "ERROR") - self.result['changed'] = False - self.result['response'] = self.msg - - return self - - def get_want(self, config): - """ - Get all the device related information from playbook that is needed to be - add/update/delete/resync device in Cisco Catalyst Center. - Parameters: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - config (dict): A dictionary containing device-related information from the playbook. - Returns: - dict: A dictionary containing the extracted device parameters and other relevant information. - Description: - Retrieve all the device-related information from the playbook needed for adding, updating, deleting, - or resyncing devices in Cisco Catalyst Center. - """ - - want = {} - device_params = self.get_device_params(config) - want["device_params"] = device_params - - self.want = want - self.msg = "Successfully collected all parameters from the playbook " - self.status = "success" - self.log("Desired State (want): {0}".format(str(self.want)), "INFO") - - return self - - def get_diff_merged(self, config): - """ - Merge and process differences between existing devices and desired device configuration in Cisco Catalyst Center. - Parameters: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - config (dict): A dictionary containing the desired device configuration and relevant information from the playbook. - Returns: - object: An instance of the class with updated results and status based on the processing of differences. - Description: - The function processes the differences and, depending on the changes required, it may add, update, - or resynchronize devices in Cisco Catalyst Center. - The updated results and status are stored in the class instance for further use. - """ - devices_to_add = self.have["device_not_in_ccc"] - device_type = self.config[0].get("type", "NETWORK_DEVICE") - device_resynced = self.config[0].get("device_resync", False) - device_updated = self.config[0].get("device_updated", False) - device_reboot = self.config[0].get("reboot_device", False) - credential_update = self.config[0].get("credential_update", False) - - config['type'] = device_type - if device_type == "FIREPOWER_MANAGEMENT_SYSTEM": - config['http_port'] = self.config[0].get("http_port", "443") - - config['ip_address_list'] = devices_to_add - - if not config['ip_address_list']: - self.msg = "Devices '{0}' already present in Cisco Catalyst Center".format(self.have['devices_in_playbook']) - self.log(self.msg, "INFO") - self.result['changed'] = False - self.result['response'] = self.msg - else: - # To add the devices in inventory - input_params = self.want.get("device_params") - device_params = input_params.copy() - - if not device_params['snmpVersion']: - device_params['snmpVersion'] = "v3" - device_params['ipAddress'] = config['ip_address_list'] - - if device_params['snmpVersion'] == "v2": - params_to_remove = ["snmpAuthPassphrase", "snmpAuthProtocol", "snmpMode", "snmpPrivPassphrase", "snmpPrivProtocol", "snmpUserName"] - for param in params_to_remove: - device_params.pop(param, None) - - if not device_params['snmpROCommunity']: - self.status = "failed" - self.msg = "Required parameter 'snmpROCommunity' for adding device with snmmp version v2 is not present" - self.result['msg'] = self.msg - self.log(self.msg, "ERROR") - return self - else: - if not device_params['snmpMode']: - device_params['snmpMode'] = "AUTHPRIV" - - if not device_params['cliTransport']: - device_params['cliTransport'] = "ssh" + if not device_params['cliTransport']: + device_params['cliTransport'] = "ssh" if not device_params['snmpPrivProtocol']: device_params['snmpPrivProtocol'] = "AES128" @@ -4403,24 +3375,6 @@ def get_diff_merged(self, config): if self.config[0].get('provision_wireless_device'): self.provisioned_wireless_devices().check_return_status() - # Create the tag for the device or interface - if self.config[0].get('create_tag'): - tag_params = self.config[0].get('create_tag') - self.create_tag(tag_params).check_return_status() - - # Update the tag for the device or interface - if self.config[0].get('update_tag'): - tag_params = self.config[0].get('update_tag') - self.update_tag(tag_params).check_return_status() - - # Assigning the device members to the tag - if self.config[0].get('assign_device_member_to_tag'): - self.assign_device_member_to_tag().check_return_status() - - # Assigning the interface members of the devices to the tag - if self.config[0].get('assign_interface_member_to_tag'): - self.assign_interface_member_to_tag().check_return_status() - if device_resynced: self.resync_devices().check_return_status() @@ -4449,52 +3403,6 @@ def get_diff_deleted(self, config): device_to_delete = self.get_device_ips_from_config_priority() self.result['msg'] = [] - if self.config[0].get('remove_device_tag_member'): - tag_params = self.config[0].get('remove_device_tag_member') - tag_name = tag_params.get('tag_name') - - if not tag_name: - self.status = "failed" - self.msg = """Cannot perform the remove task of tag from device member, 'tag_name' - is required to perform this task """ - self.log(self.msg, "ERROR") - self.result['response'] = self.msg - return self - - self.remove_device_tag_member(tag_name).check_return_status() - return self - - if self.config[0].get('remove_interface_tag_member'): - tag_params = self.config[0].get('remove_interface_tag_member') - tag_name = tag_params.get('tag_name') - interface_name_list = tag_params.get('interface_names_list') - - if not tag_name or not interface_name_list: - self.status = "failed" - self.msg = """Cannot perform the remove task of tag from interface member, 'tag_name/interface_name' - is required to perform this task """ - self.log(self.msg, "ERROR") - self.result['response'] = self.msg - return self - - self.remove_interface_tag_member(tag_name, interface_name_list).check_return_status() - return self - - if self.config[0].get('delete_tag'): - tag_params = self.config[0].get('delete_tag') - tag_name = tag_params.get('tag_name') - - if not tag_name: - self.status = "failed" - self.msg = """Cannot perform the remove task of tag from device member, 'tag_name' - is required to perform this task """ - self.log(self.msg, "ERROR") - self.result['response'] = self.msg - return self - - self.delete_tag(tag_name).check_return_status() - return self - if self.config[0].get('add_user_defined_field'): udf_field_list = self.config[0].get('add_user_defined_field') for udf in udf_field_list: @@ -4660,8 +3568,6 @@ def verify_diff_merged(self, config): if not devices_to_add: self.status = "success" - if not self.have['devices_in_playbook']: - msg = "Device addition task not needed as devices are already present in Cisco Caalyst Center." msg = """Requested device(s) '{0}' have been successfully added to the Cisco Catalyst Center and their addition has been verified.""".format(str(self.have['devices_in_playbook'])) self.log(msg, "INFO") From cd10ac787782b5eab723cf8d44afcb0c17477981 Mon Sep 17 00:00:00 2001 From: bvargasre Date: Tue, 12 Mar 2024 16:09:00 -0600 Subject: [PATCH 10/17] Update supported versions and minimum Ansible requirement --- README.md | 2 +- changelogs/changelog.yaml | 6 ++++++ galaxy.yml | 2 +- meta/runtime.yml | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2a3014b43c..2492d75b9e 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The following table shows the supported versions. | 2.2.2.3 | 3.3.1 | 2.3.3 | | 2.2.3.3 | 6.4.0 | 2.4.11 | | 2.3.3.0 | 6.6.4 | 2.5.5 | -| 2.3.5.3 | 6.12.0 | 2.6.0 | +| 2.3.5.3 | 6.13.0 | 2.6.0 | If your Ansible collection is older please consider updating it first. diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 64df79e0e0..be441c8b97 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -829,3 +829,9 @@ releases: Renamed argument from 'ip_address' to 'ip_address_list'. - pnp_workflow_manager - Adding fix for Stackswitch getting changed to normal switch post editing the device's info. - pnp_intent - Adding fix for Stackswitch getting changed to normal switch post editing the device's info. + 6.13.0: + release_date: "2024-03-12" + changes: + release_summary: Changes the minimum supported version of Ansible to v2.14.0 + minor_changes: + - Changes the minimum supported version from Ansible v2.9.10 to v2.14.0 \ No newline at end of file diff --git a/galaxy.yml b/galaxy.yml index 69be2d0998..3bb04b5107 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,7 +1,7 @@ --- namespace: cisco name: dnac -version: 6.12.0 +version: 6.13.0 readme: README.md authors: - Rafael Campos diff --git a/meta/runtime.yml b/meta/runtime.yml index 1f18fd7267..bcb6dcedc2 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -1,2 +1,2 @@ --- -requires_ansible: '>=2.9.10' \ No newline at end of file +requires_ansible: '>=2.14.0' \ No newline at end of file From fc4bce152f8cf854a2c3d266cbcff05a9b8cf5a4 Mon Sep 17 00:00:00 2001 From: bvargasre Date: Tue, 12 Mar 2024 17:08:10 -0600 Subject: [PATCH 11/17] Remove outdated Ansible versions from sanity_tests.yml --- .github/workflows/sanity_tests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/sanity_tests.yml b/.github/workflows/sanity_tests.yml index 2fe518e686..32d46d4788 100644 --- a/.github/workflows/sanity_tests.yml +++ b/.github/workflows/sanity_tests.yml @@ -14,8 +14,6 @@ jobs: strategy: matrix: ansible: - - stable-2.12 - - stable-2.13 - stable-2.14 - stable-2.15 - devel From dfc0b29c4849ce05912c170cfb27c43be4ae153f Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 13 Mar 2024 10:53:06 +0530 Subject: [PATCH 12/17] =?UTF-8?q?Fix=20the=20issue=20while=20creating=20bu?= =?UTF-8?q?ilding=20site=20without=20optional=20parameter=20=E2=80=98count?= =?UTF-8?q?ry=E2=80=99,=20Address=20the=20issue=20of=20Swim=20to=20skip=20?= =?UTF-8?q?unmatched=20filtered=20devices=20when=20distribution=20and=20ac?= =?UTF-8?q?tivating=20images=20changes=20return=20status=20to=20success=20?= =?UTF-8?q?instead=20of=20failed,=20Add=20the=20log=20message=20while=20ed?= =?UTF-8?q?iting=20the=20device=20role,=20Return=20correct=20log=20message?= =?UTF-8?q?=20if=20customer=20wants=20to=20update=20management=20IP=20of?= =?UTF-8?q?=20device=20which=20is=20not=20present=20in=20Catalyst=20Center?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/modules/inventory_intent.py | 54 +++++++++++++++++-- plugins/modules/inventory_workflow_manager.py | 54 +++++++++++++++++-- plugins/modules/site_intent.py | 18 ++++--- plugins/modules/site_workflow_manager.py | 18 ++++--- plugins/modules/swim_intent.py | 12 ++--- plugins/modules/swim_workflow_manager.py | 24 ++++----- 6 files changed, 138 insertions(+), 42 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index a5b5f5d55c..0d667edcb7 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -956,7 +956,7 @@ def validate_input(self): def get_device_ips_from_config_priority(self): """ Retrieve device IPs based on the configuration. - Args: + Parameters: - self (object): An instance of a class used for interacting with Cisco Cisco Catalyst Center. Returns: list: A list containing device IPs. @@ -1329,6 +1329,7 @@ def export_device_details(self): self.log(self.msg, "INFO") self.status = "success" self.result['changed'] = True + self.result['response'] = self.msg except Exception as e: self.msg = "Error while exporting device details into CSV file for device(s): '{0}'".format(str(device_ips)) @@ -2762,7 +2763,7 @@ def clear_mac_address(self, interface_id, deploy_mode, interface_name): def update_interface_detail_of_device(self, device_to_update): """ Update interface details for a device in Cisco Catalyst Center. - Args: + Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_to_update (list): A list of IP addresses of devices to be updated. Returns: @@ -2938,6 +2939,39 @@ def check_device_update_execution_response(self, response, device_ip): return self + def is_device_exist_in_ccc(self, device_ip): + """ + Check if a device with the given IP exists in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + device_ip (str): The IP address of the device to check. + Returns: + bool: True if the device exists, False otherwise. + Description: + This method queries Cisco Catalyst Center to check if a device with the specified + management IP address exists. If the device exists, it returns True; otherwise, + it returns False. If an error occurs during the process, it logs an error message + and raises an exception. + """ + + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"managementIpAddress": device_ip} + ) + response = response.get('response') + if not response: + self.log("Device with given IP '{0}' is not present in Cisco Catalyst Center", "INFO") + return False + + return True + + except Exception as e: + error_message = "Error while getting the response of device from Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") + raise Exception(error_message) + def get_want(self, config): """ Get all the device related information from playbook that is needed to be @@ -2990,6 +3024,16 @@ def get_diff_merged(self, config): config['ip_address_list'] = devices_to_add + if self.config[0].get('update_mgmt_ipaddresslist'): + device_ip = self.config[0].get('update_mgmt_ipaddresslist')[0].get('existMgmtIpAddress') + is_device_exists = self.is_device_exist_in_ccc(device_ip) + + if not is_device_exists: + self.status = "failed" + self.msg = "Device '{0}' not present in Cisco Catalyst Center so we cannot update the Management IP address".format(device_ip) + self.log(self.msg, "ERROR") + return self + if not config['ip_address_list']: self.msg = "Devices '{0}' already present in Cisco Catalyst Center".format(self.have['devices_in_playbook']) self.log(self.msg, "INFO") @@ -3267,6 +3311,7 @@ def get_diff_merged(self, config): self.status = "failed" self.msg = "Mandatory parameter (role) to update Device Role is missing" self.log(self.msg, "WARNING") + self.result['response'] = self.msg return self # Check if the same role of device is present in dnac then no need to change the state @@ -3309,8 +3354,8 @@ def get_diff_merged(self, config): if 'successfully' in progress or 'succesfully' in progress: self.status = "success" self.result['changed'] = True - self.result['response'] = execution_details - self.msg = "Device(s) '{0}' role updated successfully".format(str(device_to_update)) + self.msg = "Device(s) '{0}' role updated successfully to '{1}'".format(str(device_to_update), device_role_args.get('role')) + self.result['response'] = self.msg self.log(self.msg, "INFO") break elif execution_details.get("isError"): @@ -3321,6 +3366,7 @@ def get_diff_merged(self, config): else: self.msg = "Device role updation get failed" self.log(self.msg, "ERROR") + self.result['response'] = self.msg break except Exception as e: diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index 9c8e01a24b..df4062aa9b 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -956,7 +956,7 @@ def validate_input(self): def get_device_ips_from_config_priority(self): """ Retrieve device IPs based on the configuration. - Args: + Parameters: - self (object): An instance of a class used for interacting with Cisco Cisco Catalyst Center. Returns: list: A list containing device IPs. @@ -1328,6 +1328,7 @@ def export_device_details(self): self.log(self.msg, "INFO") self.status = "success" self.result['changed'] = True + self.result['response'] = self.msg except Exception as e: self.msg = "Error while exporting device details into CSV file for device(s): '{0}'".format(str(device_ips)) @@ -2756,7 +2757,7 @@ def clear_mac_address(self, interface_id, deploy_mode, interface_name): def update_interface_detail_of_device(self, device_to_update): """ Update interface details for a device in Cisco Catalyst Center. - Args: + Parameters: self (object): An instance of a class used for interacting with Cisco Catalyst Center. device_to_update (list): A list of IP addresses of devices to be updated. Returns: @@ -2932,6 +2933,39 @@ def check_device_update_execution_response(self, response, device_ip): return self + def is_device_exist_in_ccc(self, device_ip): + """ + Check if a device with the given IP exists in Cisco Catalyst Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + device_ip (str): The IP address of the device to check. + Returns: + bool: True if the device exists, False otherwise. + Description: + This method queries Cisco Catalyst Center to check if a device with the specified + management IP address exists. If the device exists, it returns True; otherwise, + it returns False. If an error occurs during the process, it logs an error message + and raises an exception. + """ + + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"managementIpAddress": device_ip} + ) + response = response.get('response') + if not response: + self.log("Device with given IP '{0}' is not present in Cisco Catalyst Center", "INFO") + return False + + return True + + except Exception as e: + error_message = "Error while getting the response of device from Cisco Catalyst Center: {0}".format(str(e)) + self.log(error_message, "ERROR") + raise Exception(error_message) + def get_want(self, config): """ Get all the device related information from playbook that is needed to be @@ -2983,6 +3017,16 @@ def get_diff_merged(self, config): config['ip_address_list'] = devices_to_add + if self.config[0].get('update_mgmt_ipaddresslist'): + device_ip = self.config[0].get('update_mgmt_ipaddresslist')[0].get('existMgmtIpAddress') + is_device_exists = self.is_device_exist_in_ccc(device_ip) + + if not is_device_exists: + self.status = "failed" + self.msg = "Device '{0}' not present in Cisco Catalyst Center so we cannot update the Management IP address".format(device_ip) + self.log(self.msg, "ERROR") + return self + if not config['ip_address_list']: self.msg = "Devices '{0}' already present in Cisco Catalyst Center".format(self.have['devices_in_playbook']) self.log(self.msg, "INFO") @@ -3260,6 +3304,7 @@ def get_diff_merged(self, config): self.status = "failed" self.msg = "Mandatory parameter (role) to update Device Role is missing" self.log(self.msg, "WARNING") + self.result['response'] = self.msg return self # Check if the same role of device is present in ccc then no need to change the state @@ -3302,9 +3347,9 @@ def get_diff_merged(self, config): if 'successfully' in progress or 'succesfully' in progress: self.status = "success" self.result['changed'] = True - self.result['response'] = execution_details - self.msg = "Device(s) '{0}' role updated successfully".format(str(device_to_update)) + self.msg = "Device(s) '{0}' role updated successfully to '{1}'".format(str(device_to_update), device_role_args.get('role')) self.log(self.msg, "INFO") + self.result['response'] = self.msg break elif execution_details.get("isError"): self.status = "failed" @@ -3314,6 +3359,7 @@ def get_diff_merged(self, config): else: self.msg = "Device role updation get failed" self.log(self.msg, "ERROR") + self.result['response'] = self.msg break except Exception as e: diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 2411b7b8df..8d439ff03f 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -645,7 +645,7 @@ def is_building_updated(self, updated_site, requested_site): updated_site['parentName'] == requested_site['parentName'] and self.compare_float_values(updated_site['latitude'], requested_site['latitude']) and self.compare_float_values(updated_site['longitude'], requested_site['longitude']) and - (requested_site['address'] is None or updated_site['address'] == requested_site['address']) + ('address' in requested_site and (requested_site['address'] is None or updated_site.get('address') == requested_site['address'])) ) def is_floor_updated(self, updated_site, requested_site): @@ -807,11 +807,13 @@ def get_diff_merged(self, config): else: # Creating New Site site_params = self.want.get("site_params") + site_params['site']['building'] = {key: value for key, value in site_params['site']['building'].items() if value is not None} + response = self.dnac._exec( family="sites", function='create_site', op_modifies=True, - params=self.want.get("site_params"), + params=site_params, ) self.log("Received API response from 'create_site': {0}".format(str(response)), "DEBUG") site_created = True @@ -832,9 +834,9 @@ def get_diff_merged(self, config): break if site_updated: - log_msg = "Site - {0} Updated Successfully".format(self.want.get("site_name")) - self.log(log_msg, "INFO") - self.result['msg'] = log_msg + self.msg = "Site - {0} Updated Successfully".format(self.want.get("site_name")) + self.log(self.msg, "INFO") + self.result['msg'] = self.msg self.result['response'].update({"siteId": self.have.get("site_id")}) else: @@ -842,10 +844,10 @@ def get_diff_merged(self, config): (site_exists, current_site) = self.site_exists() if site_exists: - log_msg = "Site '{0}' created successfully".format(self.want.get("site_name")) - self.log(log_msg, "INFO") + self.msg = "Site '{0}' created successfully".format(self.want.get("site_name")) + self.log(self.msg, "INFO") self.log("Current site (have): {0}".format(str(current_site)), "DEBUG") - self.result['msg'] = log_msg + self.result['msg'] = self.msg self.result['response'].update({"siteId": current_site.get('site_id')}) return self diff --git a/plugins/modules/site_workflow_manager.py b/plugins/modules/site_workflow_manager.py index 4e253a2ba5..75c081f77a 100644 --- a/plugins/modules/site_workflow_manager.py +++ b/plugins/modules/site_workflow_manager.py @@ -644,7 +644,7 @@ def is_building_updated(self, updated_site, requested_site): updated_site['parentName'] == requested_site['parentName'] and self.compare_float_values(updated_site['latitude'], requested_site['latitude']) and self.compare_float_values(updated_site['longitude'], requested_site['longitude']) and - (requested_site['address'] is None or updated_site['address'] == requested_site['address']) + ('address' in requested_site and (requested_site['address'] is None or updated_site.get('address') == requested_site['address'])) ) def is_floor_updated(self, updated_site, requested_site): @@ -806,11 +806,13 @@ def get_diff_merged(self, config): else: # Creating New Site site_params = self.want.get("site_params") + site_params['site']['building'] = {key: value for key, value in site_params['site']['building'].items() if value is not None} + response = self.dnac._exec( family="sites", function='create_site', op_modifies=True, - params=self.want.get("site_params"), + params=site_params, ) self.log("Received API response from 'create_site': {0}".format(str(response)), "DEBUG") site_created = True @@ -831,9 +833,9 @@ def get_diff_merged(self, config): break if site_updated: - log_msg = "Site - {0} Updated Successfully".format(self.want.get("site_name")) - self.log(log_msg, "INFO") - self.result['msg'] = log_msg + self.msg = "Site - {0} Updated Successfully".format(self.want.get("site_name")) + self.log(self.msg, "INFO") + self.result['msg'] = self.msg self.result['response'].update({"siteId": self.have.get("site_id")}) else: @@ -841,10 +843,10 @@ def get_diff_merged(self, config): (site_exists, current_site) = self.site_exists() if site_exists: - log_msg = "Site '{0}' created successfully".format(self.want.get("site_name")) - self.log(log_msg, "INFO") + self.msg = "Site '{0}' created successfully".format(self.want.get("site_name")) + self.log(self.msg, "INFO") self.log("Current site (have): {0}".format(str(current_site)), "DEBUG") - self.result['msg'] = log_msg + self.result['msg'] = self.msg self.result['response'].update({"siteId": current_site.get('site_id')}) return self diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index a5d8fe61c5..440fa56d63 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -1365,10 +1365,10 @@ def get_diff_distribution(self): return self if len(device_uuid_list) == 0: - self.status = "failed" - self.msg = "Image Distribution cannot proceed due to the absence of device(s)" + self.status = "success" + self.msg = "No matched device(s) for swim image distribution task." self.result['msg'] = self.msg - self.log(self.msg, "ERROR") + self.log(self.msg, "WARNING") return self self.log("Device UUIDs involved in Image Distribution: {0}".format(str(device_uuid_list)), "INFO") @@ -1508,10 +1508,10 @@ def get_diff_activation(self): return self if len(device_uuid_list) == 0: - self.status = "failed" - self.msg = "No devices found for Image Activation" + self.status = "success" + self.msg = "No matched device(s) for swim image activation task." self.result['msg'] = self.msg - self.log(self.msg, "ERROR") + self.log(self.msg, "WARNING") return self self.log("Device UUIDs involved in Image Activation: {0}".format(str(device_uuid_list)), "INFO") diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index b40596e5ad..4f5cb0b1c4 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -1305,9 +1305,11 @@ def get_diff_distribution(self): device_series_name = distribution_details.get("device_series_name") device_uuid_list = self.get_device_uuids(site_name, device_family, device_role, device_series_name) image_id = self.have.get("distribution_image_id") + self.complete_successful_distribution = False + self.partial_successful_distribution = False + self.single_device_distribution = False if self.have.get("distribution_device_id"): - self.single_device_distribution = False distribution_params = dict( payload=[dict( deviceUuid=self.have.get("distribution_device_id"), @@ -1351,18 +1353,16 @@ def get_diff_distribution(self): return self if len(device_uuid_list) == 0: - self.status = "failed" - self.msg = "Image Distribution cannot proceed due to the absence of device(s)" + self.status = "success" + self.msg = "No matched device(s) for swim image distribution task." self.result['msg'] = self.msg - self.log(self.msg, "ERROR") + self.log(self.msg, "WARNING") return self self.log("Device UUIDs involved in Image Distribution: {0}".format(str(device_uuid_list)), "INFO") device_distribution_count = 0 device_ips_list = [] - self.complete_successful_distribution = False - self.partial_successful_distribution = False for device_uuid in device_uuid_list: device_management_ip = self.get_device_ip_from_id(device_uuid) @@ -1443,9 +1443,11 @@ def get_diff_activation(self): device_series_name = activation_details.get("device_series_name") device_uuid_list = self.get_device_uuids(site_name, device_family, device_role, device_series_name) image_id = self.have.get("activation_image_id") + self.complete_successful_activation = False + self.partial_successful_activation = False + self.single_device_activation = False if self.have.get("activation_device_id"): - self.single_device_activation = False payload = [dict( activateLowerImageVersion=activation_details.get("activate_lower_image_version"), deviceUpgradeMode=activation_details.get("device_upgrade_mode"), @@ -1494,17 +1496,15 @@ def get_diff_activation(self): return self if len(device_uuid_list) == 0: - self.status = "failed" - self.msg = "No devices found for Image Activation" + self.status = "success" + self.msg = "No matched device(s) for swim image activation task." self.result['msg'] = self.msg - self.log(self.msg, "ERROR") + self.log(self.msg, "WARNING") return self self.log("Device UUIDs involved in Image Activation: {0}".format(str(device_uuid_list)), "INFO") device_activation_count = 0 device_ips_list = [] - self.complete_successful_activation = False - self.partial_successful_activation = False for device_uuid in device_uuid_list: device_management_ip = self.get_device_ip_from_id(device_uuid) From 77e14721fbfdefe1a6284c780878f2bcde77fdf8 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 13 Mar 2024 11:00:40 +0530 Subject: [PATCH 13/17] swim distribution/activation flag added in begining --- plugins/modules/swim_intent.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 440fa56d63..7325d5a537 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -1319,9 +1319,12 @@ def get_diff_distribution(self): device_series_name = distribution_details.get("device_series_name") device_uuid_list = self.get_device_uuids(site_name, device_family, device_role, device_series_name) image_id = self.have.get("distribution_image_id") + self.complete_successful_distribution = False + self.partial_successful_distribution = False + self.single_device_distribution = False if self.have.get("distribution_device_id"): - self.single_device_distribution = False + distribution_params = dict( payload=[dict( deviceUuid=self.have.get("distribution_device_id"), @@ -1375,8 +1378,6 @@ def get_diff_distribution(self): device_distribution_count = 0 device_ips_list = [] - self.complete_successful_distribution = False - self.partial_successful_distribution = False for device_uuid in device_uuid_list: device_management_ip = self.get_device_ip_from_id(device_uuid) @@ -1457,9 +1458,11 @@ def get_diff_activation(self): device_series_name = activation_details.get("device_series_name") device_uuid_list = self.get_device_uuids(site_name, device_family, device_role, device_series_name) image_id = self.have.get("activation_image_id") + self.complete_successful_activation = False + self.partial_successful_activation = False + self.single_device_activation = False if self.have.get("activation_device_id"): - self.single_device_activation = False payload = [dict( activateLowerImageVersion=activation_details.get("activate_lower_image_version"), deviceUpgradeMode=activation_details.get("device_upgrade_mode"), @@ -1517,8 +1520,6 @@ def get_diff_activation(self): self.log("Device UUIDs involved in Image Activation: {0}".format(str(device_uuid_list)), "INFO") device_activation_count = 0 device_ips_list = [] - self.complete_successful_activation = False - self.partial_successful_activation = False for device_uuid in device_uuid_list: device_management_ip = self.get_device_ip_from_id(device_uuid) From e586b722066af60b99c8bf72409113bd4a3570e9 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 13 Mar 2024 11:20:26 +0530 Subject: [PATCH 14/17] change code to support python older versions --- plugins/modules/site_intent.py | 7 ++++++- plugins/modules/site_workflow_manager.py | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 8d439ff03f..2d3cdaebc4 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -807,7 +807,12 @@ def get_diff_merged(self, config): else: # Creating New Site site_params = self.want.get("site_params") - site_params['site']['building'] = {key: value for key, value in site_params['site']['building'].items() if value is not None} + if site_params['site']['building']: + building_details = {} + for key, value in site_params['site']['building'].items(): + if value is not None: + building_details[key] = value + site_params['site']['building'] = building_details response = self.dnac._exec( family="sites", diff --git a/plugins/modules/site_workflow_manager.py b/plugins/modules/site_workflow_manager.py index 75c081f77a..8d62cf12ba 100644 --- a/plugins/modules/site_workflow_manager.py +++ b/plugins/modules/site_workflow_manager.py @@ -806,7 +806,12 @@ def get_diff_merged(self, config): else: # Creating New Site site_params = self.want.get("site_params") - site_params['site']['building'] = {key: value for key, value in site_params['site']['building'].items() if value is not None} + if site_params['site']['building']: + building_details = {} + for key, value in site_params['site']['building'].items(): + if value is not None: + building_details[key] = value + site_params['site']['building'] = building_details response = self.dnac._exec( family="sites", From e97c240de5298549be6d2b9f4accc5a3f2097bbc Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 13 Mar 2024 14:31:28 +0530 Subject: [PATCH 15/17] fix the issue of updating management ip for device with snmp_version v2 --- plugins/modules/inventory_intent.py | 5 +++++ plugins/modules/inventory_workflow_manager.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 0d667edcb7..9acba58ce9 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -3198,6 +3198,8 @@ def get_diff_merged(self, config): if device_data['snmpv3_privacy_password']: csv_data_dict['snmp_auth_passphrase'] = device_data['snmpv3_auth_password'] csv_data_dict['snmp_priv_passphrase'] = device_data['snmpv3_privacy_password'] + else: + csv_data_dict['snmp_username'] = None device_key_mapping = { 'username': 'userName', @@ -3256,6 +3258,9 @@ def get_diff_merged(self, config): for param in params_to_remove: playbook_params.pop(param, None) + if not playbook_params['snmpROCommunity']: + playbook_params['snmpROCommunity'] = device_data.get('snmp_community', None) + try: if playbook_params['updateMgmtIPaddressList']: new_mgmt_ipaddress = playbook_params['updateMgmtIPaddressList'][0]['newMgmtIpAddress'] diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index df4062aa9b..8027728bd2 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -3191,6 +3191,8 @@ def get_diff_merged(self, config): if device_data['snmpv3_privacy_password']: csv_data_dict['snmp_auth_passphrase'] = device_data['snmpv3_auth_password'] csv_data_dict['snmp_priv_passphrase'] = device_data['snmpv3_privacy_password'] + else: + csv_data_dict['snmp_username'] = None device_key_mapping = { 'username': 'userName', @@ -3249,6 +3251,9 @@ def get_diff_merged(self, config): for param in params_to_remove: playbook_params.pop(param, None) + if not playbook_params['snmpROCommunity']: + playbook_params['snmpROCommunity'] = device_data.get('snmp_community', None) + try: if playbook_params['updateMgmtIPaddressList']: new_mgmt_ipaddress = playbook_params['updateMgmtIPaddressList'][0]['newMgmtIpAddress'] From a47a0a29ffa947d32eceaa9245f5c8c41e53eae0 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 13 Mar 2024 15:51:06 +0530 Subject: [PATCH 16/17] Fix the issue of Un-tagging image if it's inherited from parent site and return failed with log message, address review comments --- plugins/modules/inventory_intent.py | 7 ++-- plugins/modules/inventory_workflow_manager.py | 7 ++-- plugins/modules/swim_intent.py | 35 +++++++++++++------ plugins/modules/swim_workflow_manager.py | 35 +++++++++++++------ 4 files changed, 58 insertions(+), 26 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 9acba58ce9..c15769840d 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -2962,13 +2962,13 @@ def is_device_exist_in_ccc(self, device_ip): ) response = response.get('response') if not response: - self.log("Device with given IP '{0}' is not present in Cisco Catalyst Center", "INFO") + self.log("Device with given IP '{0}' is not present in Cisco Catalyst Center".format(device_ip), "INFO") return False return True except Exception as e: - error_message = "Error while getting the response of device from Cisco Catalyst Center: {0}".format(str(e)) + error_message = "Error while getting the response of device '{0}' from Cisco Catalyst Center: {1}".format(device_ip, str(e)) self.log(error_message, "ERROR") raise Exception(error_message) @@ -3030,7 +3030,8 @@ def get_diff_merged(self, config): if not is_device_exists: self.status = "failed" - self.msg = "Device '{0}' not present in Cisco Catalyst Center so we cannot update the Management IP address".format(device_ip) + self.msg = """Unable to update the Management IP address because the device with IP '{0}' is not + found in Cisco Catalyst Center.""".format(device_ip) self.log(self.msg, "ERROR") return self diff --git a/plugins/modules/inventory_workflow_manager.py b/plugins/modules/inventory_workflow_manager.py index 8027728bd2..9f8baf912e 100644 --- a/plugins/modules/inventory_workflow_manager.py +++ b/plugins/modules/inventory_workflow_manager.py @@ -2956,13 +2956,13 @@ def is_device_exist_in_ccc(self, device_ip): ) response = response.get('response') if not response: - self.log("Device with given IP '{0}' is not present in Cisco Catalyst Center", "INFO") + self.log("Device with given IP '{0}' is not present in Cisco Catalyst Center".format(device_ip), "INFO") return False return True except Exception as e: - error_message = "Error while getting the response of device from Cisco Catalyst Center: {0}".format(str(e)) + error_message = "Error while getting the response of device '{0}' from Cisco Catalyst Center: {1}".format(device_ip, str(e)) self.log(error_message, "ERROR") raise Exception(error_message) @@ -3023,7 +3023,8 @@ def get_diff_merged(self, config): if not is_device_exists: self.status = "failed" - self.msg = "Device '{0}' not present in Cisco Catalyst Center so we cannot update the Management IP address".format(device_ip) + self.msg = """Unable to update the Management IP address because the device with IP '{0}' is not + found in Cisco Catalyst Center.""".format(device_ip) self.log(self.msg, "ERROR") return self diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 7325d5a537..6ad5e44b45 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -1239,15 +1239,27 @@ def get_diff_tagging(self): ) self.log("Received API response from 'remove_golden_tag_for_image': {0}".format(str(response)), "DEBUG") - if response: - task_details = {} - task_id = response.get("response").get("taskId") + if not response: + self.status = "failed" + self.msg = "Did not get the response of API so cannot check the Golden tagging status of image - {0}".format(image_name) + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + return self + + task_details = {} + task_id = response.get("response").get("taskId") + + while(True): task_details = self.get_task_details(task_id) - if not task_details.get("isError"): - self.result['changed'] = True - self.result['msg'] = task_details.get("progress") + + if not task_details.get("isError") and 'successful' in task_details.get("progress"): self.status = "success" - self.result['response'] = task_details if task_details else response + self.result['changed'] = True + self.msg = task_details.get("progress") + self.result['msg'] = self.msg + self.result['response'] = self.msg + self.log(self.msg, "INFO") + break elif task_details.get("isError"): failure_reason = task_details.get("failureReason", "") if failure_reason and "An inheritted tag cannot be un-tagged" in failure_reason: @@ -1256,13 +1268,16 @@ def get_diff_tagging(self): self.msg = failure_reason self.result['msg'] = failure_reason self.log(self.msg, "ERROR") + self.result['response'] = self.msg + break else: error_message = task_details.get("failureReason", "Error: while tagging/un-tagging the golden swim image.") self.status = "failed" self.msg = error_message self.result['msg'] = error_message self.log(self.msg, "ERROR") - self.result['response'] = self.msg + self.result['response'] = self.msg + break return self @@ -1369,7 +1384,7 @@ def get_diff_distribution(self): if len(device_uuid_list) == 0: self.status = "success" - self.msg = "No matched device(s) for swim image distribution task." + self.msg = "The SWIM image distribution task could not proceed because no eligible devices were found." self.result['msg'] = self.msg self.log(self.msg, "WARNING") return self @@ -1512,7 +1527,7 @@ def get_diff_activation(self): if len(device_uuid_list) == 0: self.status = "success" - self.msg = "No matched device(s) for swim image activation task." + self.msg = "The SWIM image activation task could not proceed because no eligible devices were found." self.result['msg'] = self.msg self.log(self.msg, "WARNING") return self diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index 4f5cb0b1c4..83a1d87648 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -1225,15 +1225,27 @@ def get_diff_tagging(self): ) self.log("Received API response from 'remove_golden_tag_for_image': {0}".format(str(response)), "DEBUG") - if response: - task_details = {} - task_id = response.get("response").get("taskId") + if not response: + self.status = "failed" + self.msg = "Did not get the response of API so cannot check the Golden tagging status of image - {0}".format(image_name) + self.log(self.msg, "ERROR") + self.result['response'] = self.msg + return self + + task_details = {} + task_id = response.get("response").get("taskId") + + while(True): task_details = self.get_task_details(task_id) - if not task_details.get("isError"): - self.result['changed'] = True - self.result['msg'] = task_details.get("progress") + + if not task_details.get("isError") and 'successful' in task_details.get("progress"): self.status = "success" - self.result['response'] = task_details if task_details else response + self.result['changed'] = True + self.msg = task_details.get("progress") + self.result['msg'] = self.msg + self.result['response'] = self.msg + self.log(self.msg, "INFO") + break elif task_details.get("isError"): failure_reason = task_details.get("failureReason", "") if failure_reason and "An inheritted tag cannot be un-tagged" in failure_reason: @@ -1242,13 +1254,16 @@ def get_diff_tagging(self): self.msg = failure_reason self.result['msg'] = failure_reason self.log(self.msg, "ERROR") + self.result['response'] = self.msg + break else: error_message = task_details.get("failureReason", "Error: while tagging/un-tagging the golden swim image.") self.status = "failed" self.msg = error_message self.result['msg'] = error_message self.log(self.msg, "ERROR") - self.result['response'] = self.msg + self.result['response'] = self.msg + break return self @@ -1354,7 +1369,7 @@ def get_diff_distribution(self): if len(device_uuid_list) == 0: self.status = "success" - self.msg = "No matched device(s) for swim image distribution task." + self.msg = "The SWIM image distribution task could not proceed because no eligible devices were found" self.result['msg'] = self.msg self.log(self.msg, "WARNING") return self @@ -1497,7 +1512,7 @@ def get_diff_activation(self): if len(device_uuid_list) == 0: self.status = "success" - self.msg = "No matched device(s) for swim image activation task." + self.msg = "The SWIM image activation task could not proceed because no eligible devices were found." self.result['msg'] = self.msg self.log(self.msg, "WARNING") return self From 95bd7ba8d4635fb137021cdf50eb0b96a6501f20 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 13 Mar 2024 15:56:54 +0530 Subject: [PATCH 17/17] Add whitspaces after the while loop --- plugins/modules/swim_intent.py | 2 +- plugins/modules/swim_workflow_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 6ad5e44b45..4bd66acf35 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -1249,7 +1249,7 @@ def get_diff_tagging(self): task_details = {} task_id = response.get("response").get("taskId") - while(True): + while True: task_details = self.get_task_details(task_id) if not task_details.get("isError") and 'successful' in task_details.get("progress"): diff --git a/plugins/modules/swim_workflow_manager.py b/plugins/modules/swim_workflow_manager.py index 83a1d87648..a7a0ee39c4 100644 --- a/plugins/modules/swim_workflow_manager.py +++ b/plugins/modules/swim_workflow_manager.py @@ -1235,7 +1235,7 @@ def get_diff_tagging(self): task_details = {} task_id = response.get("response").get("taskId") - while(True): + while True: task_details = self.get_task_details(task_id) if not task_details.get("isError") and 'successful' in task_details.get("progress"):