From 7a15f55c39121d59c33f92e533129a973ddba5c2 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Tue, 19 Dec 2023 12:22:07 +0000 Subject: [PATCH 01/63] Making the previously kept template_pnp_intent playbook to underscore based naming --- playbooks/template_pnp_intent.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/playbooks/template_pnp_intent.yml b/playbooks/template_pnp_intent.yml index 4ce25e16d0..11530fa70c 100644 --- a/playbooks/template_pnp_intent.yml +++ b/playbooks/template_pnp_intent.yml @@ -8,7 +8,7 @@ # # Project Info Section # - - name: Test project template + - name: Test project template cisco.dnac.template_intent: dnac_host: "{{ dnac_host }}" dnac_port: "{{ dnac_port }}" @@ -20,15 +20,15 @@ state: "merged" #ignore_errors: true #Enable this to continue execution even the task fails config: - - projectName: "{{ item.proj_name }}" - templateContent: "{{ item.device_config }}" + - project_name: "{{ item.proj_name }}" + template_content: "{{ item.device_config }}" language: "{{ item.language }}" deviceTypes: - - productFamily: "{{ item.family }}" - softwareType: "{{ item.type }}" - softwareVariant: "{{ item.variant }}" - templateName: "{{ item.temp_name }}" - versionDescription: "{{ item.description }}" + - product_family: "{{ item.family }}" + software_type: "{{ item.type }}" + software_variant: "{{ item.variant }}" + template_name: "{{ item.temp_name }}" + version_description: "{{ item.description }}" register: template_result with_items: '{{ template_details }}' tags: @@ -50,11 +50,11 @@ template_name: "{{ item.temp_name }}" image_name: "{{ item.image_name }}" device_version: "{{ item.device_version }}" - deviceInfo: - serialNumber: "{{ item.device_number }}" - hostname: "{{ item.device_name}}" - state: "{{ item.device_state }}" - pid: "{{ item.device_id }}" + device_info: + - serialNumber: "{{ item.device_number }}" + hostname: "{{ item.device_name}}" + state: "{{ item.device_state }}" + pid: "{{ item.device_id }}" register: pnp_result with_items: '{{ device_details }}' tags: From 8816a62be86eeed3ade8b56c7c7bed3a6a054b12 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Tue, 19 Dec 2023 15:44:48 +0000 Subject: [PATCH 02/63] Making the previously kept template_pnp_intent playbook to underscore based naming --- playbooks/template_pnp_intent.yml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/playbooks/template_pnp_intent.yml b/playbooks/template_pnp_intent.yml index 5bca327240..37b43d8ceb 100644 --- a/playbooks/template_pnp_intent.yml +++ b/playbooks/template_pnp_intent.yml @@ -20,15 +20,16 @@ state: "merged" #ignore_errors: true #Enable this to continue execution even the task fails config: - - project_name: "{{ item.proj_name }}" - template_content: "{{ item.device_config }}" - language: "{{ item.language }}" - deviceTypes: - - product_family: "{{ item.family }}" - software_type: "{{ item.type }}" - software_variant: "{{ item.variant }}" - template_name: "{{ item.temp_name }}" - version_description: "{{ item.description }}" + - configuration_templates: + project_name: "{{ item.proj_name }}" + template_content: "{{ item.device_config }}" + language: "{{ item.language }}" + device_types: + - product_family: "{{ item.family }}" + software_type: "{{ item.type }}" + software_variant: "{{ item.variant }}" + template_name: "{{ item.temp_name }}" + version_description: "{{ item.description }}" register: template_result with_items: '{{ template_details }}' tags: From cad8564ad9bcc8413fe203c15a7592f122067038 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 19 Dec 2023 21:40:18 +0530 Subject: [PATCH 03/63] Validating the DNAC after applying the config --- playbooks/network_settings_intent.yml | 2 + plugins/modules/network_settings_intent.py | 114 ++++++++++++++++++++- 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/playbooks/network_settings_intent.yml b/playbooks/network_settings_intent.yml index 8f803522ce..46ddc3deca 100644 --- a/playbooks/network_settings_intent.yml +++ b/playbooks/network_settings_intent.yml @@ -17,6 +17,7 @@ dnac_verify: "{{ dnac_verify }}" dnac_debug: "{{ dnac_debug }}" dnac_log: True + config_verify: True state: merged config: - global_pool_details: @@ -94,6 +95,7 @@ dnac_verify: "{{ dnac_verify }}" dnac_debug: "{{ dnac_debug }}" dnac_log: True + config_verify: True state: deleted config: - global_pool_details: diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index 6712fbc458..7b8128188e 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -25,6 +25,10 @@ author: Muthu Rakesh (@MUTHU-RAKESH-27) Madhan Sankaranarayanan (@madhansansel) options: + config_verify: + description: Set to True to verify the config in the Cisco DNA Center. + type: bool + default: False state: description: The state of Cisco DNA Center after module completion. type: str @@ -637,7 +641,7 @@ def get_global_pool_params(self, pool_info): "dnsServerIps": pool_info.get("dnsServerIps"), "ipPoolCidr": pool_info.get("ipPoolCidr"), "ipPoolName": pool_info.get("ipPoolName"), - "type": pool_info.get("type") + "type": pool_info.get("ipPoolType").capitalize() }] } } @@ -1207,7 +1211,7 @@ def get_want_global_pool(self, global_ippool): # Copy existing Global Pool information if the desired configuration is not provided want_ippool.update({ "IpAddressSpace": have_ippool.get("IpAddressSpace"), - "type": have_ippool.get("ipPoolType"), + "type": have_ippool.get("type"), "ipPoolCidr": have_ippool.get("ipPoolCidr") }) want_ippool.update({}) @@ -1949,6 +1953,108 @@ def get_diff_deleted(self, config): return self + def validate_dnac_config(self, state, config): + """ + Validating the DNAC configuration with the playbook details. + + Parameters: + state (str) - State of the task. merged - Create or + Update global pool, reserve pool , network servers else + deleted - Delete global pool, reserve pool, network servers. + + config (dict) - Playbook details containing Global Pool, + Reserved Pool, and Network Management configuration. + + Returns: + None + """ + + self.get_have(config) + self.log(str(self.have)) + self.log(str(self.want)) + if state == "merged": + if config.get("global_pool_details") is not None: + obj_params = [ + ("settings", "settings"), + ] + self.log(str(self.want.get("wantGlobal"))) + self.log(str(self.have.get("globalPool").get("details"))) + if self.requires_update(self.have.get("globalPool").get("details"), + self.want.get("wantGlobal"), obj_params): + self.msg = "Global Pool Config is not applied to the DNAC" + self.status = "failed" + return self + + self.log("Successfully validated Global Pool") + self.result.get("response")[0].get("globalPool").update({"Validation": "Success"}) + + if config.get("reserve_pool_details") is not None: + obj_params = [ + ("name", "name"), + ("type", "type"), + ("ipv6AddressSpace", "ipv6AddressSpace"), + ("ipv4GlobalPool", "ipv4GlobalPool"), + ("ipv4Prefix", "ipv4Prefix"), + ("ipv4PrefixLength", "ipv4PrefixLength"), + ("ipv4GateWay", "ipv4GateWay"), + ("ipv4DhcpServers", "ipv4DhcpServers"), + ("ipv4DnsServers", "ipv4DnsServers"), + ("ipv6GateWay", "ipv6GateWay"), + ("ipv6DhcpServers", "ipv6DhcpServers"), + ("ipv6DnsServers", "ipv6DnsServers"), + ("ipv4TotalHost", "ipv4TotalHost"), + ("slaacSupport", "slaacSupport") + ] + if self.requires_update(self.have.get("reservePool").get("details"), + self.want.get("wantReserve"), obj_params): + self.log(str(self.want.get("wantReserve"))) + self.log(str(self.have.get("reservePool").get("details"))) + self.msg = "Reserve Pool Config is not applied to the DNAC" + self.status = "failed" + return self + + self.log("Successfully validated the Reserve Pool") + self.result.get("response")[1].get("reservePool").update({"Validation": "Success"}) + + if config.get("network_management_details") is not None: + obj_params = [ + ("settings", "settings"), + ("site_name", "site_name") + ] + if self.requires_update(self.have.get("network").get("net_details"), + self.want.get("wantNetwork"), obj_params): + self.msg = "Network Functions Config is not applied to the DNAC" + self.status = "failed" + return self + + self.log("Successfully validated the Network Functions") + self.result.get("response")[2].get("network").update({"Validation": "Success"}) + elif state == "deleted": + if config.get("global_pool_details") is not None: + global_pool_exists = self.have.get("globalPool").get("exists") + if global_pool_exists: + self.msg = "Global Pool Config is not applied to the DNAC" + self.status = "failed" + return self + + self.log("Successfully validated Global Pool") + self.result.get("response")[0].get("globalPool").update({"Validation": "Success"}) + + if config.get("reserve_pool_details") is not None: + reserve_pool_exists = self.have.get("reservePool").get("exists") + if reserve_pool_exists: + self.msg = "Reserve Pool Config is not applied to the DNAC" + self.status = "failed" + return self + + self.log("Successfully validated Reserve Pool") + self.result.get("response")[1].get("reservePool").update({"Validation": "Success"}) + + self.msg = "Successfully validated the Global Pool, Reserve Pool, \ + Network functions" + self.status = "success" + return self + def reset_values(self): """ Reset all neccessary attributes to default values @@ -1978,6 +2084,7 @@ def main(): "dnac_version": {"type": 'str', "default": '2.2.3.3'}, "dnac_debug": {"type": 'bool', "default": False}, "dnac_log": {"type": 'bool', "default": False}, + "config_verify": {"type": 'bool', "default": False}, "config": {"type": 'list', "required": True, "elements": 'dict'}, "state": {"default": 'merged', "choices": ['merged', 'deleted']}, "validate_response_schema": {"type": 'bool', "default": True}, @@ -1987,6 +2094,7 @@ def main(): module = AnsibleModule(argument_spec=element_spec, supports_check_mode=False) dnac_network = DnacNetwork(module) state = dnac_network.params.get("state") + config_verify = dnac_network.params.get("config_verify") if state not in dnac_network.supported_states: dnac_network.status = "invalid" dnac_network.msg = "State {0} is invalid".format(state) @@ -2000,6 +2108,8 @@ def main(): if state != "deleted": dnac_network.get_want(config).check_return_status() dnac_network.get_diff_state_apply[state](config).check_return_status() + if config_verify: + dnac_network.validate_dnac_config(state, config).check_return_status() module.exit_json(**dnac_network.result) From cb2ca4501d6c5f7c455feff5aeaceb89bcbb9d15 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 19 Dec 2023 22:37:34 +0530 Subject: [PATCH 04/63] Changed the description for config_verify --- plugins/modules/network_settings_intent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index 7b8128188e..ec006dddde 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -26,7 +26,7 @@ Madhan Sankaranarayanan (@madhansansel) options: config_verify: - description: Set to True to verify the config in the Cisco DNA Center. + description: Set to True to verify the Cisco DNA Center after applying the playbook config. type: bool default: False state: From 057f7601b437c04c515a1cd23bcde763dc76af22 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Wed, 20 Dec 2023 05:42:11 +0000 Subject: [PATCH 05/63] Adding the corrected playbook for template_pnp --- playbooks/device_details.yml | 6 +++--- playbooks/template_pnp_intent.yml | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/playbooks/device_details.yml b/playbooks/device_details.yml index ae4d017ffd..4a26326e17 100644 --- a/playbooks/device_details.yml +++ b/playbooks/device_details.yml @@ -48,7 +48,7 @@ device_details: device_number: "AB2425L8M7" device_name: "Cat9k-1" device_state: "Unclaimed" - device_id: "C9300-25UX" + device_id: "C9300-48UXM" - site_name: "Global/Chennai/Trill" image_name: "cat9k_iosxe.17.04.01.SPA.bin" proj_name: "Onboarding Configuration" @@ -57,7 +57,7 @@ device_details: device_number: "CD2425L8M7" device_name: "Cat9k-2" device_state: "Unclaimed" - device_id: "C9300-25UX" + device_id: "C9300-48UXM" - site_name: "Global/Chennai/Trill" image_name: "cat9k_iosxe.17.04.01.SPA.bin" proj_name: "Onboarding Configuration" @@ -66,4 +66,4 @@ device_details: device_number: "EF2425L8M7" device_name: "Cat9k-3" device_state: "Unclaimed" - device_id: "C9300-25UX" + device_id: "C9300-48UXM" diff --git a/playbooks/template_pnp_intent.yml b/playbooks/template_pnp_intent.yml index 37b43d8ceb..8dfe099abb 100644 --- a/playbooks/template_pnp_intent.yml +++ b/playbooks/template_pnp_intent.yml @@ -1,6 +1,6 @@ -- hosts: dnac_servers +- hosts: localhost vars_files: - - credentials_245.yml + - "{{ CLUSTERFILE }}" - device_details.yml gather_facts: false connection: local @@ -25,11 +25,11 @@ template_content: "{{ item.device_config }}" language: "{{ item.language }}" device_types: - - product_family: "{{ item.family }}" - software_type: "{{ item.type }}" - software_variant: "{{ item.variant }}" - template_name: "{{ item.temp_name }}" - version_description: "{{ item.description }}" + - product_family: "{{ item.family }}" + software_type: "{{ item.type }}" + software_variant: "{{ item.variant }}" + template_name: "{{ item.temp_name }}" + version_description: "{{ item.description }}" register: template_result with_items: '{{ template_details }}' tags: @@ -52,7 +52,7 @@ image_name: "{{ item.image_name }}" device_version: "{{ item.device_version }}" device_info: - - serialNumber: "{{ item.device_number }}" + - serial_number: "{{ item.device_number }}" hostname: "{{ item.device_name}}" state: "{{ item.device_state }}" pid: "{{ item.device_id }}" From 3cdcef68bce1fc9d7d684686b8708475832753c5 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Wed, 20 Dec 2023 05:58:11 +0000 Subject: [PATCH 06/63] Adding the corrected playbook for template_pnp --- playbooks/template_pnp_intent.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/playbooks/template_pnp_intent.yml b/playbooks/template_pnp_intent.yml index 8dfe099abb..c559628276 100644 --- a/playbooks/template_pnp_intent.yml +++ b/playbooks/template_pnp_intent.yml @@ -22,14 +22,14 @@ config: - configuration_templates: project_name: "{{ item.proj_name }}" + template_name: "{{ item.temp_name }}" template_content: "{{ item.device_config }}" + version_description: "{{ item.description }}" language: "{{ item.language }}" - device_types: - - product_family: "{{ item.family }}" software_type: "{{ item.type }}" software_variant: "{{ item.variant }}" - template_name: "{{ item.temp_name }}" - version_description: "{{ item.description }}" + device_types: + - product_family: "{{ item.family }}" register: template_result with_items: '{{ template_details }}' tags: @@ -50,7 +50,6 @@ project_name: "{{ item.proj_name }}" template_name: "{{ item.temp_name }}" image_name: "{{ item.image_name }}" - device_version: "{{ item.device_version }}" device_info: - serial_number: "{{ item.device_number }}" hostname: "{{ item.device_name}}" From 98b9cbb7b8753e9581b9f8f243aa4c4530e41d0f Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 20 Dec 2023 18:36:51 +0530 Subject: [PATCH 07/63] Provision Wireless devices, add examples in documentation --- plugins/modules/inventory_intent.py | 297 ++++++++++++++++++++++++++-- 1 file changed, 280 insertions(+), 17 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 7f2d4207ad..5e38376fcc 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -60,7 +60,7 @@ description: Device's http username. Required for Adding Compute,Firepower Management Devices. type: str id: - description: Id path parameter that is Device ID. Required for Deleting/Updating Device Roles. + description: Id path parameter that is Device ID. type: str ip_address: description: Device's ipAddress. Required for Adding/Updating/Deleting/Resyncing Device except Meraki Devices. @@ -186,7 +186,33 @@ parameters: description: List of device parameters that needs to be exported to file. type: str - + managed_ap_locations: + description: Location of the sites allocated for the APs + type: list + elements: str + dynamic_interfaces: + description: Interface details of the wireless device + type: list + elements: dict + suboptions: + interface_ip_address: + description: Ip Address allocated to the interface + type: str + interface_netmask_in_cidr: + description: Ip Address allocated to the interface + type: int + interface_gateway: + description: Ip Address allocated to the interface + type: str + lag_or_port_number: + description: Ip Address allocated to the interface + type: int + vlan_id: + description: Ip Address allocated to the interface + type: int + interface_name: + description: Ip Address allocated to the interface + type: str requirements: - dnacentersdk >= 2.5.5 @@ -273,7 +299,8 @@ dnac_log: False state: merged config: - - ip_address: string + - ip_address: + - string http_username: string http_password: string http_port: string @@ -317,7 +344,8 @@ dnac_log: False state: merged config: - - ip_address: string + - ip_address: + - string http_username: string http_password: string http_port: string @@ -336,7 +364,8 @@ dnac_log: False state: merged config: - - ip_address: string + - ip_address: + - string snmp_auth_passphrase: string snmp_auth_protocol: string snmp_mode: string @@ -360,10 +389,37 @@ dnac_log: False state: merged config: - - ip_address: string + - ip_address: + - string provision_wired_device: site_name: string +- name: Associate Wireless Devices to site and Provisioned it in Inventory + cisco.dnac.inventory_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: False + state: merged + config: + - ip_address: + - string + provision_wireless_device: + - site_name: string + managed_ap_locations: + - string + dynamic_interfaces: + - interface_ip_address: string + interface_netmask_in_cidr: int + interface_gateway: string + lag_or_port_number: int + vlan_id: int + interface_name: string + - name: Update Device Role with IP Address cisco.dnac.inventory_intent: dnac_host: "{{dnac_host}}" @@ -376,7 +432,8 @@ dnac_log: False state: merged config: - - ip_address: string + - ip_address: + - string device_updated: true update_device_role: role: string @@ -394,7 +451,8 @@ dnac_log: False state: merged config: - - ip_address: string + - ip_address: + - string device_updated: true update_interface_details: description: str @@ -414,7 +472,8 @@ dnac_log: False state: merged config: - - ip_address: string + - ip_address: + - string export_device_list: password: str operation_enum: str @@ -432,7 +491,8 @@ dnac_log: False state: merged config: - - ip_address: string + - ip_address: + - string add_user_defined_field: name: string description: string @@ -450,7 +510,8 @@ dnac_log: False state: merged config: - - ip_address: string + - ip_address: + - string device_resync: true force_sync: false @@ -466,10 +527,11 @@ dnac_log: False state: merged config: - - ip_address: string + - ip_address: + - string reboot_device: true -- name: Delete Provision/Unprovisioned Devices by IP Address +- name: Delete Provision/Unprovision Devices by IP Address cisco.dnac.inventory_intent: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" @@ -481,7 +543,8 @@ dnac_log: False state: deleted config: - - ip_address: string + - ip_address: + - string clean_config: false - name: Delete Global User Defined Field with name @@ -496,7 +559,8 @@ dnac_log: False state: deleted config: - - ip_address: string + - ip_address: + - string add_user_defined_field: name: string @@ -597,6 +661,7 @@ def validate_input(self): 'upate_interface_details': {'type': 'dict'}, 'deployment_mode': {'default': 'Deploy', 'type': 'str'}, 'provision_wired_device': {'type': 'dict'}, + 'provision_wireless_device': {'type': 'list', 'elements': 'dict'}, 'export_device_list': {'type': 'dict'} } @@ -1151,6 +1216,199 @@ def provisioned_wired_device(self): return self + def get_wireless_param(self, device_ip): + """ + Get wireless provisioning parameters for a device. + Parameters: + self (object): An instance of a class used for interacting with Cisco DNA Center. + device_ip (str): The IP address of the device for which to retrieve wireless provisioning parameters. + Returns: + wireless_param (list of dict): A list containing a dictionary with wireless provisioning parameters. + Description: + This function constructs a list containing a dictionary with wireless provisioning parameters based on the + configuration provided in the playbook. It validates the managed AP locations, ensuring they are of type "floor." + The function then queries Cisco DNA Center to get network device details using the provided device IP. + If the device is not found, the function returns the class instance with appropriate status and log messages and + returns the wireless provisioning parameters containing site information, managed AP + locations, dynamic interfaces, and device name. + """ + + temp_dict = self.config[0]['provision_wireless_device'][0] + wireless_param = [ + { + 'site': temp_dict['site_name'], + 'managedAPLocations': temp_dict['managed_ap_locations'], + } + ] + + for ap_loc in wireless_param[0]["managedAPLocations"]: + + if self.get_site_type(site_name=ap_loc) != "floor": + self.status = "failed" + self.msg = "Managed AP Location must be a floor" + self.log(self.msg) + self.module.fail_json(msg=self.msg, response=[self.msg]) + + wireless_param[0]["dynamicInterfaces"] = [] + + for interface in temp_dict.get("dynamic_interfaces"): + interface_dict = { + "interfaceIPAddress": interface.get("interface_ip_address"), + "interfaceNetmaskInCIDR": interface.get("interface_netmask_in_cidr"), + "interfaceGateway": interface.get("interface_gateway"), + "lagOrPortNumber": interface.get("lag_or_port_number"), + "vlanId": interface.get("vlan_id"), + "interfaceName": interface.get("interface_name") + } + wireless_param[0]["dynamicInterfaces"].append(interface_dict) + + response = self.dnac_apply['exec']( + family="devices", + function='get_network_device_by_ip', + params={"ip_address": device_ip} + ) + if not response: + self.status = "failed" + self.msg = "Device Host name is not present in the Cisco DNA Center" + self.log(self.msg) + self.module.fail_json(msg=self.msg, response=[self.msg]) + + response = response.get("response") + wireless_param[0]["deviceName"] = response.get("hostname") + + return wireless_param + + def get_site_type(self, site_name=None): + """ + Get the type of a site in Cisco DNA Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco DNA Center. + site_name (str): The name of the site for which to retrieve the type. + Returns: + site_type (str or None): The type of the specified site, or None if the site is not found. + Description: + This function queries Cisco DNA Center to retrieve the type of a specified site. It uses the + get_site API with the provided site name, extracts the site type from the response, and returns it. + If the specified site is not found, the function returns None, and an appropriate log message is generated. + """ + + try: + site_type = None + response = self.dnac_apply['exec']( + family="sites", + function='get_site', + params={"name": site_name}, + ) + + if not response: + self.msg = "Site - {0} not found".format(site_name) + self.log(self.msg) + return site_type + + self.log(str(response)) + site = response.get("response") + site_additional_info = site[0].get("additionalInfo") + + for item in site_additional_info: + if item["nameSpace"] == "Location": + site_type = item.get("attributes").get("type") + + except Exception: + self.module.fail_json(msg="Site not found", response=[]) + + return site_type + + def provisioned_wireless_devices(self, device_ips): + """ + Provision Wireless devices in Cisco DNA Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco DNA Center. + device_ips (list): List of IP addresses of the devices to be provisioned. + Returns: + self (object): An instance of the class with updated result, status, and log. + Description: + This function performs wireless provisioning for the provided list of device IP addresses. + It iterates through each device, retrieves provisioning parameters using the get_wireless_param function, + and then calls the Cisco DNA Center API for wireless provisioning. If all devices are already provisioned, + it returns success with a relevant message. + """ + + provision_count, already_provision_count = 0, 0 + + for device_ip in device_ips: + try: + # Collect the device parameters from the playbook to perform wireless provisioing + provisioning_params = self.get_wireless_param(device_ip) + + # Now we have provisioning_param so we can do wireless provisioning + response = self.dnac_apply['exec']( + family="wireless", + function="provision", + op_modifies=True, + params=provisioning_params, + ) + + if response.get("status") == "failed": + description = response.get("description") + error_msg = "Cannot do Provisioning for Wireless device {0} beacuse of {1}".format(device_ip, description) + self.log(error_msg) + continue + + task_id = response.get("taskId") + + while True: + execution_details = self.get_task_details(task_id) + progress = execution_details.get("progress") + self.log(progress) + + if 'TASK_PROVISION' in progress: + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + provision_count += 1 + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Wireless Device Provisioning get failed because of {0}".format(failure_reason) + else: + self.msg = "Wireless Device Provisioning get failed" + break + except Exception as e: + # Not returning from here as there might be possiblity that for some devices it comes into exception + # but for others it gets provision successfully or If some devices are already provsioned + error_message = "Error while Provisioning the device {0} in Cisco DNA Center - {1}".format(device_ip, str(e)) + self.log(error_message) + + if "already provisioned" in str(e): + self.log(str(e)) + already_provision_count += 1 + + # Check If all the devices are already provsioned, return from here only + if already_provision_count == len(device_ips): + self.status = "success" + self.msg = "All the Wired Devices given in the playbook are already Provisioned" + self.log(self.msg) + self.result['response'] = self.msg + self.result['changed'] = False + return self + + if provision_count == len(device_ips): + self.status = "success" + self.result['changed'] = True + msg = "Wireless Device get provisioned Successfully !!" + elif provision_count == 0: + self.status = "failed" + msg = "Wireless Device Provisioning get failed" + else: + self.status = "success" + self.result['changed'] = True + msg = "Wireless Device get provisioned Successfully Partially for {0} devices!!".format(provision_count) + self.log(msg) + + return self + def get_udf_id(self, field_name): """ Get the ID of a Global User Defined Field in Cisco DNA Center based on its name. @@ -1245,7 +1503,7 @@ def get_have(self, config): if ip not in device_in_dnac: device_not_in_dnac.append(ip) - log("Device Exists in Cisco DNA Center : " + str(device_in_dnac)) + self.log("Device Exists in Cisco DNA Center : " + str(device_in_dnac)) have["want_device"] = want_device have["device_in_dnac"] = device_in_dnac have["device_not_in_dnac"] = device_not_in_dnac @@ -1700,10 +1958,15 @@ def get_diff_merged(self, config): self.log(error_message) raise Exception(error_message) - # Once device get added we will assign device to site and Provisioned it + # Once Wired device get added we will assign device to site and Provisioned it if self.config[0].get('provision_wired_device'): self.provisioned_wired_device().check_return_status() + # Once Wireless device get added we will assign device to site and Provisioned it + if self.config[0].get('provision_wireless_device'): + device_ips = self.config[0]['ip_address'] + self.provisioned_wireless_devices(device_ips).check_return_status() + if device_resynced: self.resync_devices().check_return_status() From ee106c8175e851bd365c3e519ce1e0ca32fc23dc Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 21 Dec 2023 19:05:03 +0530 Subject: [PATCH 08/63] Remove redundant task status call by making seperate API and add docstring for each of them --- plugins/modules/inventory_intent.py | 234 ++++++++++++++++++++-------- 1 file changed, 165 insertions(+), 69 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 5e38376fcc..52085f4035 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -1126,6 +1126,129 @@ def reboot_access_points(self): return self + def handle_successful_provisioning(self, device_ip, execution_details, device_type): + """ + Handle successful provisioning of Wired/Wireless device. + Parameters: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - device_ip (str): The IP address of the provisioned device. + - execution_details (str): Details of the provisioning execution. + - device_type (str): The type or category of the provisioned device(Wired/Wireless). + Return: + None + Description: + This method updates the status, result, and logs the successful provisioning of a device. + """ + + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.log("{0} Device {1} provisioned successfully!!".format(device_type, device_ip)) + + def handle_failed_provisioning(self, device_ip, execution_details, device_type): + """ + Handle failed provisioning of Wired/Wireless device. + Parameters: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - device_ip (str): The IP address of the device that failed provisioning. + - execution_details (dict): Details of the failed provisioning execution in key "failureReason" indicating reason for failure. + - device_type (str): The type or category of the provisioned device(Wired/Wireless). + Return: + None + Description: + This method updates the status, result, and logs the failure of provisioning for a device. + """ + + self.status = "failed" + failure_reason = execution_details.get("failureReason", "Unknown failure reason") + self.msg = f"{0} Device Provisioning failed for {1} because of {2}".format(device_type, device_ip, failure_reason) + self.log(self.msg) + + def handle_provisioning_exception(self, device_ip, exception, device_type): + """ + Handle an exception during the provisioning process of Wired/Wireless device.. + Parameters: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - device_ip (str): The IP address of the device involved in provisioning. + - exception (Exception): The exception raised during provisioning. + - device_type (str): The type or category of the provisioned device(Wired/Wireless). + Return: + None + Description: + This method logs an error message indicating an exception occurred during the provisioning process for a device. + """ + + error_message = "Error while Provisioning the {0} device {1} in Cisco DNA Center - {2}".format(device_type, device_ip, str(exception)) + self.log(error_message) + + def handle_all_already_provisioned(self, device_ips, device_type): + """ + Handle successful provisioning for all devices(Wired/Wireless). + Parameters: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - device_type (str): The type or category of the provisioned device(Wired/Wireless). + Return: + None + Description: + This method updates the status, result, and logs the successful provisioning for all devices(Wired/Wireless). + """ + + self.status = "success" + self.msg = "All the {0} Devices - {1} given in the playbook are already Provisioned".format(device_type, str(device_ips)) + self.log(self.msg) + self.result['response'] = self.msg + self.result['changed'] = False + + def handle_all_provisioned(self, device_type): + """ + Handle successful provisioning for all devices(Wired/Wireless). + Parameters: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - device_type (str): The type or category of the provisioned devices(Wired/Wireless). + Return: + None + Description: + This method updates the status, result, and logs the successful provisioning for all devices(Wired/Wireless). + """ + + self.status = "success" + self.result['changed'] = True + self.log("All {0} Devices provisioned successfully!!".format(device_type)) + + def handle_all_failed_provision(self, device_type): + """ + Handle failure of provisioning for all devices(Wired/Wireless). + Parameters: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - device_type (str): The type or category of the devices(Wired/Wireless). + Return: + None + Description: + This method updates the status and logs a failure message indicating that + provisioning failed for all devices of a specific type. + """ + + self.status = "failed" + self.log("{0} Device Provisioning failed for all devices".format(device_type)) + + def handle_partially_provisioned(self, provision_count, device_type): + """ + Handle partial success in provisioning for devices(Wired/Wireless). + Parameters: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - provision_count (int): The count of devices that were successfully provisioned. + - device_type (str): The type or category of the provisioned devices(Wired/Wireless). + Return: + None + Description: + This method updates the status, result, and logs a partial success message indicating that provisioning was successful + for a certain number of devices(Wired/Wireless). + """ + + self.status = "success" + self.result['changed'] = True + self.log("{0} Devices provisioned successfully partially for {1} devices".format(device_type, provision_count)) + def provisioned_wired_device(self): """ Provision wired devices in Cisco DNA Center. @@ -1141,7 +1264,15 @@ def provisioned_wired_device(self): site_name = self.config[0]['provision_wired_device']['site_name'] device_ips = self.config[0]['ip_address'] + device_type = "Wired" provision_count, already_provision_count = 0, 0 + + if not site_name and not device_ips: + self.status = "failed" + self.msg = "Site/Devices are required for Provisioning of Wired Devices." + self.log(self.msg) + return self + provision_wired_params = { 'siteNameHierarchy': site_name } @@ -1171,48 +1302,30 @@ def provisioned_wired_device(self): self.log(progress) if 'TASK_PROVISION' in progress: - self.result['changed'] = True - self.result['response'] = execution_details + self.handle_successful_provisioning(device_ip, execution_details, device_type) provision_count += 1 break elif execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Device Provisioning get failed because of {0}".format(failure_reason) - else: - self.msg = "Device Provisioning get failed" + self.handle_failed_provisioning(device_ip, execution_details, device_type) break except Exception as e: # Not returning from here as there might be possiblity that for some devices it comes into exception # but for others it gets provision successfully or If some devices are already provsioned - error_message = "Error while Provisioning the device {0} in Cisco DNA Center - {1}".format(device_ip, str(e)) - self.log(error_message) - + self.handle_provisioning_exception(device_ip, e, device_type) if "already provisioned" in str(e): self.log(str(e)) already_provision_count += 1 # Check If all the devices are already provsioned, return from here only if already_provision_count == len(device_ips): - self.status = "success" - self.msg = "All the Wired Devices given in the playbook are already Provisioned" - self.log(self.msg) - self.result['response'] = self.msg - self.result['changed'] = False - return self - - if provision_count == len(device_ips): - self.status = "success" - msg = "Wired Device get provisioned Successfully !!" + self.handle_all_already_provisioned(device_ips, device_type) + elif provision_count == len(device_ips): + self.handle_all_provisioned(device_type) elif provision_count == 0: - self.status = "failed" - msg = "Wired Device Provisioning get failed" + self.handle_all_failed_provision(device_type) else: - self.status = "success" - msg = "Wired Device get provisioned Successfully Partially for {0} devices!!".format(provision_count) - self.log(msg) + self.handle_partially_provisioned(provision_count, device_type) return self @@ -1233,25 +1346,24 @@ def get_wireless_param(self, device_ip): locations, dynamic interfaces, and device name. """ - temp_dict = self.config[0]['provision_wireless_device'][0] + wireless_config = self.config[0]['provision_wireless_device'][0] wireless_param = [ { - 'site': temp_dict['site_name'], - 'managedAPLocations': temp_dict['managed_ap_locations'], + 'site': wireless_config['site_name'], + 'managedAPLocations': wireless_config['managed_ap_locations'], } ] for ap_loc in wireless_param[0]["managedAPLocations"]: - if self.get_site_type(site_name=ap_loc) != "floor": self.status = "failed" self.msg = "Managed AP Location must be a floor" self.log(self.msg) - self.module.fail_json(msg=self.msg, response=[self.msg]) + return self wireless_param[0]["dynamicInterfaces"] = [] - for interface in temp_dict.get("dynamic_interfaces"): + for interface in wireless_config.get("dynamic_interfaces"): interface_dict = { "interfaceIPAddress": interface.get("interface_ip_address"), "interfaceNetmaskInCIDR": interface.get("interface_netmask_in_cidr"), @@ -1271,14 +1383,17 @@ def get_wireless_param(self, device_ip): self.status = "failed" self.msg = "Device Host name is not present in the Cisco DNA Center" self.log(self.msg) - self.module.fail_json(msg=self.msg, response=[self.msg]) + return self response = response.get("response") wireless_param[0]["deviceName"] = response.get("hostname") + self.wireless_param = wireless_param + self.status = "success" + self.log("Successfully collected all parameters required for Wireless Provisioing") - return wireless_param + return self - def get_site_type(self, site_name=None): + def get_site_type(self, site_name): """ Get the type of a site in Cisco DNA Center. Parameters: @@ -1334,11 +1449,13 @@ def provisioned_wireless_devices(self, device_ips): """ provision_count, already_provision_count = 0, 0 + device_type = "Wireless" for device_ip in device_ips: try: # Collect the device parameters from the playbook to perform wireless provisioing - provisioning_params = self.get_wireless_param(device_ip) + self.get_wireless_param(device_ip).check_return_status() + provisioning_params = self.wireless_param # Now we have provisioning_param so we can do wireless provisioning response = self.dnac_apply['exec']( @@ -1360,52 +1477,31 @@ def provisioned_wireless_devices(self, device_ips): execution_details = self.get_task_details(task_id) progress = execution_details.get("progress") self.log(progress) - if 'TASK_PROVISION' in progress: - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details + self.handle_successful_provisioning(device_ip, execution_details, device_type) provision_count += 1 break elif execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Wireless Device Provisioning get failed because of {0}".format(failure_reason) - else: - self.msg = "Wireless Device Provisioning get failed" + self.handle_failed_provisioning(device_ip, execution_details, device_type) break + except Exception as e: # Not returning from here as there might be possiblity that for some devices it comes into exception # but for others it gets provision successfully or If some devices are already provsioned - error_message = "Error while Provisioning the device {0} in Cisco DNA Center - {1}".format(device_ip, str(e)) - self.log(error_message) - + self.handle_provisioning_exception(device_ip, e, device_type) if "already provisioned" in str(e): self.log(str(e)) already_provision_count += 1 # Check If all the devices are already provsioned, return from here only if already_provision_count == len(device_ips): - self.status = "success" - self.msg = "All the Wired Devices given in the playbook are already Provisioned" - self.log(self.msg) - self.result['response'] = self.msg - self.result['changed'] = False - return self - - if provision_count == len(device_ips): - self.status = "success" - self.result['changed'] = True - msg = "Wireless Device get provisioned Successfully !!" + self.handle_all_already_provisioned(device_ips, device_type) + elif provision_count == len(device_ips): + self.handle_all_provisioned(device_type) elif provision_count == 0: - self.status = "failed" - msg = "Wireless Device Provisioning get failed" + self.handle_all_failed_provision(device_type) else: - self.status = "success" - self.result['changed'] = True - msg = "Wireless Device get provisioned Successfully Partially for {0} devices!!".format(provision_count) - self.log(msg) + self.handle_partially_provisioned(provision_count, device_type) return self @@ -1557,13 +1653,13 @@ def get_device_params(self, params): } if device_param.get("updateMgmtIPaddressList"): - temp_dict = device_param.get("updateMgmtIPaddressList")[0] + device_mngmt_dict = device_param.get("updateMgmtIPaddressList")[0] device_param["updateMgmtIPaddressList"][0] = {} device_param["updateMgmtIPaddressList"][0].update( { - "existMgmtIpAddress": temp_dict.get("exist_mgmt_ipaddress"), - "newMgmtIpAddress": temp_dict.get("new_mgmt_ipaddress") + "existMgmtIpAddress": device_mngmt_dict.get("exist_mgmt_ipaddress"), + "newMgmtIpAddress": device_mngmt_dict.get("new_mgmt_ipaddress") }) return device_param From 720464d692974bb91f593f5242df9afe3a427a00 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 21 Dec 2023 19:08:41 +0530 Subject: [PATCH 09/63] remove f string --- plugins/modules/inventory_intent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 52085f4035..117c687410 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -1161,7 +1161,7 @@ def handle_failed_provisioning(self, device_ip, execution_details, device_type): self.status = "failed" failure_reason = execution_details.get("failureReason", "Unknown failure reason") - self.msg = f"{0} Device Provisioning failed for {1} because of {2}".format(device_type, device_ip, failure_reason) + self.msg = "{0} Device Provisioning failed for {1} because of {2}".format(device_type, device_ip, failure_reason) self.log(self.msg) def handle_provisioning_exception(self, device_ip, exception, device_type): From c8c1a216babad9daa7d23f8ab56e232fad151b78 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 22 Dec 2023 11:00:47 +0530 Subject: [PATCH 10/63] Added the validation structure in dnac.py and addresses template id issue --- plugins/module_utils/dnac.py | 43 +++++ plugins/modules/network_settings_intent.py | 178 +++++++++++---------- plugins/modules/template_intent.py | 2 +- 3 files changed, 141 insertions(+), 82 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index b0ed83c71b..ce74a588f8 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -55,6 +55,14 @@ def __init__(self, module): 'rendered': self.get_diff_rendered, 'parsed': self.get_diff_parsed } + self.verify_diff_state_apply = {'merged': self.verify_diff_merged, + 'deleted': self.verify_diff_deleted, + 'replaced': self.verify_diff_replaced, + 'overridden': self.verify_diff_overridden, + 'gathered': self.verify_diff_gathered, + 'rendered': self.verify_diff_rendered, + 'parsed': self.verify_diff_parsed + } self.dnac_log = dnac_params.get("dnac_log") log(str(dnac_params)) self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] @@ -102,6 +110,41 @@ def get_diff_parsed(self): self.parsed = True return True + def verify_diff_merged(self): + # Implement logic to verify the merged resource configuration + self.merged = True + return self + + def verify_diff_deleted(self): + # Implement logic to verify the merged resource + self.deleted = True + return self + + def verify_diff_replaced(self): + # Implement logic to verify the replaced resource + self.replaced = True + return self + + def verify_diff_overridden(self): + # Implement logic to verify the overwritten resource + self.overridden = True + return self + + def verify_diff_gathered(self): + # Implement logic to verify the gathered data about the resource + self.gathered = True + return self + + def verify_diff_rendered(self): + # Implement logic to verify the rendered configuration template + self.rendered = True + return self + + def verify_diff_parsed(self): + # Implement logic to verify the parsed configuration file + self.parsed = True + return True + def log(self, message, frameIncrement=0): """Log messages into dnac.log file""" diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index ec006dddde..6884f356e4 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -1953,105 +1953,121 @@ def get_diff_deleted(self, config): return self - def validate_dnac_config(self, state, config): + def verify_diff_merged(self, config): """ - Validating the DNAC configuration with the playbook details. + Validating the DNAC configuration with the playbook details + when state is merged (Create/Update). Parameters: - state (str) - State of the task. merged - Create or - Update global pool, reserve pool , network servers else - deleted - Delete global pool, reserve pool, network servers. - config (dict) - Playbook details containing Global Pool, Reserved Pool, and Network Management configuration. Returns: - None + self """ self.get_have(config) self.log(str(self.have)) self.log(str(self.want)) - if state == "merged": - if config.get("global_pool_details") is not None: - obj_params = [ - ("settings", "settings"), - ] - self.log(str(self.want.get("wantGlobal"))) - self.log(str(self.have.get("globalPool").get("details"))) - if self.requires_update(self.have.get("globalPool").get("details"), - self.want.get("wantGlobal"), obj_params): - self.msg = "Global Pool Config is not applied to the DNAC" - self.status = "failed" - return self + if config.get("global_pool_details") is not None: + obj_params = [ + ("settings", "settings"), + ] + self.log(str(self.want.get("wantGlobal"))) + self.log(str(self.have.get("globalPool").get("details"))) + if self.requires_update(self.have.get("globalPool").get("details"), + self.want.get("wantGlobal"), obj_params): + self.msg = "Global Pool Config is not applied to the DNAC" + self.status = "failed" + return self - self.log("Successfully validated Global Pool") - self.result.get("response")[0].get("globalPool").update({"Validation": "Success"}) - - if config.get("reserve_pool_details") is not None: - obj_params = [ - ("name", "name"), - ("type", "type"), - ("ipv6AddressSpace", "ipv6AddressSpace"), - ("ipv4GlobalPool", "ipv4GlobalPool"), - ("ipv4Prefix", "ipv4Prefix"), - ("ipv4PrefixLength", "ipv4PrefixLength"), - ("ipv4GateWay", "ipv4GateWay"), - ("ipv4DhcpServers", "ipv4DhcpServers"), - ("ipv4DnsServers", "ipv4DnsServers"), - ("ipv6GateWay", "ipv6GateWay"), - ("ipv6DhcpServers", "ipv6DhcpServers"), - ("ipv6DnsServers", "ipv6DnsServers"), - ("ipv4TotalHost", "ipv4TotalHost"), - ("slaacSupport", "slaacSupport") - ] - if self.requires_update(self.have.get("reservePool").get("details"), - self.want.get("wantReserve"), obj_params): - self.log(str(self.want.get("wantReserve"))) - self.log(str(self.have.get("reservePool").get("details"))) - self.msg = "Reserve Pool Config is not applied to the DNAC" - self.status = "failed" - return self + self.log("Successfully validated Global Pool") + self.result.get("response")[0].get("globalPool").update({"Validation": "Success"}) - self.log("Successfully validated the Reserve Pool") - self.result.get("response")[1].get("reservePool").update({"Validation": "Success"}) - - if config.get("network_management_details") is not None: - obj_params = [ - ("settings", "settings"), - ("site_name", "site_name") - ] - if self.requires_update(self.have.get("network").get("net_details"), - self.want.get("wantNetwork"), obj_params): - self.msg = "Network Functions Config is not applied to the DNAC" - self.status = "failed" - return self + if config.get("reserve_pool_details") is not None: + obj_params = [ + ("name", "name"), + ("type", "type"), + ("ipv6AddressSpace", "ipv6AddressSpace"), + ("ipv4GlobalPool", "ipv4GlobalPool"), + ("ipv4Prefix", "ipv4Prefix"), + ("ipv4PrefixLength", "ipv4PrefixLength"), + ("ipv4GateWay", "ipv4GateWay"), + ("ipv4DhcpServers", "ipv4DhcpServers"), + ("ipv4DnsServers", "ipv4DnsServers"), + ("ipv6GateWay", "ipv6GateWay"), + ("ipv6DhcpServers", "ipv6DhcpServers"), + ("ipv6DnsServers", "ipv6DnsServers"), + ("ipv4TotalHost", "ipv4TotalHost"), + ("slaacSupport", "slaacSupport") + ] + if self.requires_update(self.have.get("reservePool").get("details"), + self.want.get("wantReserve"), obj_params): + self.log(str(self.want.get("wantReserve"))) + self.log(str(self.have.get("reservePool").get("details"))) + self.msg = "Reserve Pool Config is not applied to the DNAC" + self.status = "failed" + return self - self.log("Successfully validated the Network Functions") - self.result.get("response")[2].get("network").update({"Validation": "Success"}) - elif state == "deleted": - if config.get("global_pool_details") is not None: - global_pool_exists = self.have.get("globalPool").get("exists") - if global_pool_exists: - self.msg = "Global Pool Config is not applied to the DNAC" - self.status = "failed" - return self + self.log("Successfully validated the Reserve Pool") + self.result.get("response")[1].get("reservePool").update({"Validation": "Success"}) - self.log("Successfully validated Global Pool") - self.result.get("response")[0].get("globalPool").update({"Validation": "Success"}) + if config.get("network_management_details") is not None: + obj_params = [ + ("settings", "settings"), + ("site_name", "site_name") + ] + if self.requires_update(self.have.get("network").get("net_details"), + self.want.get("wantNetwork"), obj_params): + self.msg = "Network Functions Config is not applied to the DNAC" + self.status = "failed" + return self - if config.get("reserve_pool_details") is not None: - reserve_pool_exists = self.have.get("reservePool").get("exists") - if reserve_pool_exists: - self.msg = "Reserve Pool Config is not applied to the DNAC" - self.status = "failed" - return self + self.log("Successfully validated the Network Functions") + self.result.get("response")[2].get("network").update({"Validation": "Success"}) + + self.msg = "Successfully validated the Global Pool, Reserve Pool \ + and the Network Functions." + self.status = "success" + return self + + def verify_diff_deleted(self, config): + """ + Validating the DNAC configuration with the playbook details + when state is deleted (delete). + + Parameters: + config (dict) - Playbook details containing Global Pool, + Reserved Pool, and Network Management configuration. + + Returns: + self + """ + + self.get_have(config) + self.log(str(self.have)) + self.log(str(self.want)) + if config.get("global_pool_details") is not None: + global_pool_exists = self.have.get("globalPool").get("exists") + if global_pool_exists: + self.msg = "Global Pool Config is not applied to the DNAC" + self.status = "failed" + return self + + self.log("Successfully validated Global Pool") + self.result.get("response")[0].get("globalPool").update({"Validation": "Success"}) + + if config.get("reserve_pool_details") is not None: + reserve_pool_exists = self.have.get("reservePool").get("exists") + if reserve_pool_exists: + self.msg = "Reserve Pool Config is not applied to the DNAC" + self.status = "failed" + return self - self.log("Successfully validated Reserve Pool") - self.result.get("response")[1].get("reservePool").update({"Validation": "Success"}) + self.log("Successfully validated Reserve Pool") + self.result.get("response")[1].get("reservePool").update({"Validation": "Success"}) - self.msg = "Successfully validated the Global Pool, Reserve Pool, \ - Network functions" + self.msg = "Successfully validated the Global Pool, Reserve Pool" self.status = "success" return self @@ -2109,7 +2125,7 @@ def main(): dnac_network.get_want(config).check_return_status() dnac_network.get_diff_state_apply[state](config).check_return_status() if config_verify: - dnac_network.validate_dnac_config(state, config).check_return_status() + dnac_network.verify_diff_state_apply[state](config).check_return_status() module.exit_json(**dnac_network.result) diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index 8675417ea5..cba347caac 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -2143,7 +2143,7 @@ def create_project_or_template(self, is_create_project=False): if value is None: creation_id = task_details.get("data") else: - creation_id = value.get("data").get("templateId") + creation_id = value.get("templateId") if not creation_id: self.log("data is not found for taskid: {0}".format(task_id)) continue From 3bc4d0120389635dafe43845246c4cd48aef4e14 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 22 Dec 2023 11:15:44 +0530 Subject: [PATCH 11/63] Resolved the sanity issues --- plugins/module_utils/dnac.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index ce74a588f8..2e7e4e41cf 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -56,13 +56,13 @@ def __init__(self, module): 'parsed': self.get_diff_parsed } self.verify_diff_state_apply = {'merged': self.verify_diff_merged, - 'deleted': self.verify_diff_deleted, - 'replaced': self.verify_diff_replaced, - 'overridden': self.verify_diff_overridden, - 'gathered': self.verify_diff_gathered, - 'rendered': self.verify_diff_rendered, - 'parsed': self.verify_diff_parsed - } + 'deleted': self.verify_diff_deleted, + 'replaced': self.verify_diff_replaced, + 'overridden': self.verify_diff_overridden, + 'gathered': self.verify_diff_gathered, + 'rendered': self.verify_diff_rendered, + 'parsed': self.verify_diff_parsed + } self.dnac_log = dnac_params.get("dnac_log") log(str(dnac_params)) self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] From 305b92ce4a6df7c2d242624ac0db62bcd5775c98 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 22 Dec 2023 12:09:54 +0530 Subject: [PATCH 12/63] Addressed the PR comments --- playbooks/network_settings_intent.yml | 4 ++-- plugins/module_utils/dnac.py | 6 +++--- plugins/modules/network_settings_intent.py | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/playbooks/network_settings_intent.yml b/playbooks/network_settings_intent.yml index 46ddc3deca..e9e7fc42e3 100644 --- a/playbooks/network_settings_intent.yml +++ b/playbooks/network_settings_intent.yml @@ -17,8 +17,8 @@ dnac_verify: "{{ dnac_verify }}" dnac_debug: "{{ dnac_debug }}" dnac_log: True - config_verify: True state: merged + config_verify: True config: - global_pool_details: settings: @@ -95,8 +95,8 @@ dnac_verify: "{{ dnac_verify }}" dnac_debug: "{{ dnac_debug }}" dnac_log: True - config_verify: True state: deleted + config_verify: True config: - global_pool_details: settings: diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 2e7e4e41cf..6b22e70419 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -108,7 +108,7 @@ def get_diff_rendered(self): def get_diff_parsed(self): # Implement logic to parse a configuration file self.parsed = True - return True + return self def verify_diff_merged(self): # Implement logic to verify the merged resource configuration @@ -116,7 +116,7 @@ def verify_diff_merged(self): return self def verify_diff_deleted(self): - # Implement logic to verify the merged resource + # Implement logic to verify the deleted resource self.deleted = True return self @@ -143,7 +143,7 @@ def verify_diff_rendered(self): def verify_diff_parsed(self): # Implement logic to verify the parsed configuration file self.parsed = True - return True + return self def log(self, message, frameIncrement=0): """Log messages into dnac.log file""" diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index 6884f356e4..38699a9b2a 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -2045,8 +2045,8 @@ def verify_diff_deleted(self, config): """ self.get_have(config) - self.log(str(self.have)) - self.log(str(self.want)) + self.log("DNAC retrieved details: " + str(self.have)) + self.log("Playbook details: " + str(self.want)) if config.get("global_pool_details") is not None: global_pool_exists = self.have.get("globalPool").get("exists") if global_pool_exists: @@ -2054,7 +2054,7 @@ def verify_diff_deleted(self, config): self.status = "failed" return self - self.log("Successfully validated Global Pool") + self.log("Successfully validated absence of Global Pool") self.result.get("response")[0].get("globalPool").update({"Validation": "Success"}) if config.get("reserve_pool_details") is not None: @@ -2064,10 +2064,10 @@ def verify_diff_deleted(self, config): self.status = "failed" return self - self.log("Successfully validated Reserve Pool") + self.log("Successfully validated the absence of Reserve Pool") self.result.get("response")[1].get("reservePool").update({"Validation": "Success"}) - self.msg = "Successfully validated the Global Pool, Reserve Pool" + self.msg = "Successfully validated the absence of Global Pool/Reserve Pool" self.status = "success" return self From b429d11518223986b5e0ce24d4670125834e1901 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 22 Dec 2023 13:23:28 +0530 Subject: [PATCH 13/63] Addressed the PR comments --- plugins/modules/network_settings_intent.py | 97 ++++++++++++---------- 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index 38699a9b2a..94d8237078 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -586,6 +586,51 @@ def requires_update(self, have, want, obj_params): requested_obj.get(ansible_param)) for (dnac_param, ansible_param) in obj_params) + def get_obj_params(self, get_object): + """ + Get the required comparison obj_params value + + Parameters: + get_object (str) - identifier for the required obj_params + + Returns: + obj_params (list) - obj_params value for comparison. + """ + + try: + if get_object is "GlobalPool": + obj_params = [ + ("settings", "settings"), + ] + elif get_object is "ReservePool": + obj_params = [ + ("name", "name"), + ("type", "type"), + ("ipv6AddressSpace", "ipv6AddressSpace"), + ("ipv4GlobalPool", "ipv4GlobalPool"), + ("ipv4Prefix", "ipv4Prefix"), + ("ipv4PrefixLength", "ipv4PrefixLength"), + ("ipv4GateWay", "ipv4GateWay"), + ("ipv4DhcpServers", "ipv4DhcpServers"), + ("ipv4DnsServers", "ipv4DnsServers"), + ("ipv6GateWay", "ipv6GateWay"), + ("ipv6DhcpServers", "ipv6DhcpServers"), + ("ipv6DnsServers", "ipv6DnsServers"), + ("ipv4TotalHost", "ipv4TotalHost"), + ("slaacSupport", "slaacSupport") + ] + elif get_object is "Network": + obj_params = [ + ("settings", "settings"), + ("site_name", "site_name") + ] + else: + raise ValueError("Unexpected value: {}".format(get_object)) + except Exception as msg: + self.log("Error message:" + msg) + + return obj_params + def get_site_id(self, site_name): """ Get the site id from the site name. @@ -1639,9 +1684,7 @@ def update_global_pool(self, config): return # Pool exists, check update is required - obj_params = [ - ("settings", "settings"), - ] + obj_params = self.get_obj_params("GlobalPool") if not self.requires_update(self.have.get("globalPool").get("details"), self.want.get("wantGlobal"), obj_params): self.log("Global pool doesn't requires an update") @@ -1731,22 +1774,7 @@ def update_reserve_pool(self, config): return # Check update is required - obj_params = [ - ("name", "name"), - ("type", "type"), - ("ipv6AddressSpace", "ipv6AddressSpace"), - ("ipv4GlobalPool", "ipv4GlobalPool"), - ("ipv4Prefix", "ipv4Prefix"), - ("ipv4PrefixLength", "ipv4PrefixLength"), - ("ipv4GateWay", "ipv4GateWay"), - ("ipv4DhcpServers", "ipv4DhcpServers"), - ("ipv4DnsServers", "ipv4DnsServers"), - ("ipv6GateWay", "ipv6GateWay"), - ("ipv6DhcpServers", "ipv6DhcpServers"), - ("ipv6DnsServers", "ipv6DnsServers"), - ("ipv4TotalHost", "ipv4TotalHost"), - ("slaacSupport", "slaacSupport") - ] + obj_params = self.get_obj_params("ReservePool") if not self.requires_update(self.have.get("reservePool").get("details"), self.want.get("wantReserve"), obj_params): self.log("Reserved ip subpool doesn't require an update") @@ -1790,10 +1818,7 @@ def update_network(self, config): site_name = config.get("network_management_details").get("site_name") result_network = self.result.get("response")[2].get("network") result_network.get("response").update({site_name: {}}) - obj_params = [ - ("settings", "settings"), - ("site_name", "site_name") - ] + obj_params = self.get_obj_params("Network") # Check update is required or not if not self.requires_update(self.have.get("network").get("net_details"), @@ -1970,9 +1995,7 @@ def verify_diff_merged(self, config): self.log(str(self.have)) self.log(str(self.want)) if config.get("global_pool_details") is not None: - obj_params = [ - ("settings", "settings"), - ] + obj_params = self.get_obj_params("GlobalPool") self.log(str(self.want.get("wantGlobal"))) self.log(str(self.have.get("globalPool").get("details"))) if self.requires_update(self.have.get("globalPool").get("details"), @@ -1985,22 +2008,7 @@ def verify_diff_merged(self, config): self.result.get("response")[0].get("globalPool").update({"Validation": "Success"}) if config.get("reserve_pool_details") is not None: - obj_params = [ - ("name", "name"), - ("type", "type"), - ("ipv6AddressSpace", "ipv6AddressSpace"), - ("ipv4GlobalPool", "ipv4GlobalPool"), - ("ipv4Prefix", "ipv4Prefix"), - ("ipv4PrefixLength", "ipv4PrefixLength"), - ("ipv4GateWay", "ipv4GateWay"), - ("ipv4DhcpServers", "ipv4DhcpServers"), - ("ipv4DnsServers", "ipv4DnsServers"), - ("ipv6GateWay", "ipv6GateWay"), - ("ipv6DhcpServers", "ipv6DhcpServers"), - ("ipv6DnsServers", "ipv6DnsServers"), - ("ipv4TotalHost", "ipv4TotalHost"), - ("slaacSupport", "slaacSupport") - ] + obj_params = self.get_obj_params("ReservePool") if self.requires_update(self.have.get("reservePool").get("details"), self.want.get("wantReserve"), obj_params): self.log(str(self.want.get("wantReserve"))) @@ -2013,10 +2021,7 @@ def verify_diff_merged(self, config): self.result.get("response")[1].get("reservePool").update({"Validation": "Success"}) if config.get("network_management_details") is not None: - obj_params = [ - ("settings", "settings"), - ("site_name", "site_name") - ] + obj_params = self.get_obj_params("Network") if self.requires_update(self.have.get("network").get("net_details"), self.want.get("wantNetwork"), obj_params): self.msg = "Network Functions Config is not applied to the DNAC" From 3313af04548ce483aa0927e4aeff682959bb1aa1 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 22 Dec 2023 13:27:13 +0530 Subject: [PATCH 14/63] Addressed the PR comments --- plugins/modules/network_settings_intent.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index 94d8237078..4c6ef0415e 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -589,7 +589,7 @@ def requires_update(self, have, want, obj_params): def get_obj_params(self, get_object): """ Get the required comparison obj_params value - + Parameters: get_object (str) - identifier for the required obj_params @@ -598,11 +598,11 @@ def get_obj_params(self, get_object): """ try: - if get_object is "GlobalPool": + if get_object == "GlobalPool": obj_params = [ ("settings", "settings"), ] - elif get_object is "ReservePool": + elif get_object == "ReservePool": obj_params = [ ("name", "name"), ("type", "type"), @@ -619,13 +619,13 @@ def get_obj_params(self, get_object): ("ipv4TotalHost", "ipv4TotalHost"), ("slaacSupport", "slaacSupport") ] - elif get_object is "Network": + elif get_object == "Network": obj_params = [ ("settings", "settings"), ("site_name", "site_name") ] else: - raise ValueError("Unexpected value: {}".format(get_object)) + raise ValueError("Unexpected value: {0}".format(get_object)) except Exception as msg: self.log("Error message:" + msg) From d072bd4d45183c6c1e3fe59aad169dd4ac4b42fa Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 22 Dec 2023 14:14:47 +0530 Subject: [PATCH 15/63] Addressed the PR comments --- plugins/modules/network_settings_intent.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index 4c6ef0415e..3f58053d3a 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -423,6 +423,9 @@ def __init__(self, module): {"reservePool": {"response": {}, "msg": {}}}, {"network": {"response": {}, "msg": {}}} ] + self.global_pool_obj_params = self.get_obj_params("GlobalPool") + self.reserve_pool_obj_params = self.get_obj_params("ReservePool") + self.network_obj_params = self.get_obj_params("Network") def validate_input(self): """ @@ -1684,9 +1687,8 @@ def update_global_pool(self, config): return # Pool exists, check update is required - obj_params = self.get_obj_params("GlobalPool") if not self.requires_update(self.have.get("globalPool").get("details"), - self.want.get("wantGlobal"), obj_params): + self.want.get("wantGlobal"), self.global_pool_obj_params): self.log("Global pool doesn't requires an update") result_global_pool.get("response").get(name).update({ "Cisco DNA Center params": @@ -1774,9 +1776,8 @@ def update_reserve_pool(self, config): return # Check update is required - obj_params = self.get_obj_params("ReservePool") if not self.requires_update(self.have.get("reservePool").get("details"), - self.want.get("wantReserve"), obj_params): + self.want.get("wantReserve"), self.reserve_pool_obj_params): self.log("Reserved ip subpool doesn't require an update") result_reserve_pool.get("response").get(name) \ .update({"Cisco DNA Center params": self.have.get("reservePool").get("details")}) @@ -1818,11 +1819,10 @@ def update_network(self, config): site_name = config.get("network_management_details").get("site_name") result_network = self.result.get("response")[2].get("network") result_network.get("response").update({site_name: {}}) - obj_params = self.get_obj_params("Network") # Check update is required or not if not self.requires_update(self.have.get("network").get("net_details"), - self.want.get("wantNetwork"), obj_params): + self.want.get("wantNetwork"), self.network_obj_params): self.log("Network doesn't require an update") result_network.get("response").get(site_name).update({ @@ -1995,11 +1995,10 @@ def verify_diff_merged(self, config): self.log(str(self.have)) self.log(str(self.want)) if config.get("global_pool_details") is not None: - obj_params = self.get_obj_params("GlobalPool") self.log(str(self.want.get("wantGlobal"))) self.log(str(self.have.get("globalPool").get("details"))) if self.requires_update(self.have.get("globalPool").get("details"), - self.want.get("wantGlobal"), obj_params): + self.want.get("wantGlobal"), self.global_pool_obj_params): self.msg = "Global Pool Config is not applied to the DNAC" self.status = "failed" return self @@ -2008,9 +2007,8 @@ def verify_diff_merged(self, config): self.result.get("response")[0].get("globalPool").update({"Validation": "Success"}) if config.get("reserve_pool_details") is not None: - obj_params = self.get_obj_params("ReservePool") if self.requires_update(self.have.get("reservePool").get("details"), - self.want.get("wantReserve"), obj_params): + self.want.get("wantReserve"), self.reserve_pool_obj_params): self.log(str(self.want.get("wantReserve"))) self.log(str(self.have.get("reservePool").get("details"))) self.msg = "Reserve Pool Config is not applied to the DNAC" @@ -2021,9 +2019,8 @@ def verify_diff_merged(self, config): self.result.get("response")[1].get("reservePool").update({"Validation": "Success"}) if config.get("network_management_details") is not None: - obj_params = self.get_obj_params("Network") if self.requires_update(self.have.get("network").get("net_details"), - self.want.get("wantNetwork"), obj_params): + self.want.get("wantNetwork"), self.network_obj_params): self.msg = "Network Functions Config is not applied to the DNAC" self.status = "failed" return self From d8436d8fe5c261d769d155600bf1f377dae3f4eb Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 22 Dec 2023 16:16:05 +0530 Subject: [PATCH 16/63] Resolved a bug on the variable mismatch on project_name and template_name --- plugins/modules/template_intent.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index cba347caac..ad5056336d 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -2292,19 +2292,18 @@ def get_export_template_values(self, export_values): function='get_projects_details' ) for values in export_values: - self.log(str(values.get("projectName"))) + self.log(str(values.get("project_name"))) template_details = template_details.get("response") self.log(str(template_details)) - self.log(str(values.get("projectName"))) all_template_details = get_dict_result(template_details, "name", - values.get("projectName")) + values.get("project_name")) self.log(str(all_template_details)) all_template_details = all_template_details.get("templates") self.log(str(all_template_details)) template_detail = get_dict_result(all_template_details, "name", - values.get("templateName")) + values.get("template_name")) self.log(str(template_detail)) if template_detail is None: self.msg = "Invalid project_name and template_name in export" From f186cd9bc9a04f3f58231bc09a8e9de9f8e257a6 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 22 Dec 2023 17:18:25 +0530 Subject: [PATCH 17/63] Added the code for Validation of Site Creation/Updation/Deletion after playbook execution --- plugins/modules/site_intent.py | 224 ++++++++++++++++++++++++++------- 1 file changed, 182 insertions(+), 42 deletions(-) diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 83f412f8b2..9ec2559bae 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -25,6 +25,10 @@ Rishita Chowdhary (@rishitachowdhary) Abhishek Maheshwari (@abhishekmaheshwari) options: + config_verify: + description: Set to True to verify the Cisco DNA Center config after applying the playbook config. + type: bool + default: False state: description: The state of DNAC after module completion. type: str @@ -406,6 +410,7 @@ def get_current_site(self, site): address=location.get("attributes").get("address"), latitude=location.get("attributes").get("latitude"), longitude=location.get("attributes").get("longitude"), + country=location.get("attributes").get("country"), ) ) @@ -541,9 +546,8 @@ def get_site_params(self, params): def get_site_name(self, site): """ Get and Return the site name. - Parameters: - self (object): An instance of a class used for interacting with Cisco DNA Center. + - self (object): An instance of a class used for interacting with Cisco DNA Center. - site (dict): A dictionary containing information about the site. Returns: - str: The constructed site name. @@ -561,10 +565,97 @@ def get_site_name(self, site): return site_name + def compare_float_values(self, ele1, ele2, precision=2): + """ + Compare two floating-point values with a specified precision. + Args: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - ele1 (float): The first floating-point value to be compared. + - ele2 (float): The second floating-point value to be compared. + - precision (int, optional): The number of decimal places to consider in the comparison, Defaults to 2. + Return: + bool: True if the rounded values are equal within the specified precision, False otherwise. + Description: + This method compares two floating-point values, ele1 and ele2, by rounding them + to the specified precision and checking if the rounded values are equal. It returns + True if the rounded values are equal within the specified precision, and False otherwise. + """ + + return round(float(ele1), precision) == round(float(ele2), precision) + + def is_area_updated(self, updated_site, requested_site): + """ + Check if the area site details have been updated. + Args: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - updated_site (dict): The site details after the update. + - requested_site (dict): The site details as requested for the update. + Return: + bool: True if the area details (name and parentName) have been updated, False otherwise. + Description: + This method compares the area details (name and parentName) of the updated site + with the requested site and returns True if they are equal, indicating that the area + details have been updated. Returns False if there is a mismatch in the area site details. + """ + + return ( + updated_site['name'] == requested_site['name'] and + updated_site['parentName'] == requested_site['parentName'] + ) + + def is_building_updated(self, updated_site, requested_site): + """ + Check if the building details in a site have been updated. + Args: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - updated_site (dict): The site details after the update. + - requested_site (dict): The site details as requested for the update. + Return: + bool: True if the building details have been updated, False otherwise. + Description: + This method compares the building details of the updated site with the requested site. + It checks if the name, parentName, latitude, longitude, and address (if provided) are + equal, indicating that the building details have been updated. Returns True if the + details match, and False otherwise. + """ + + return ( + updated_site['name'] == requested_site['name'] and + 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']) + ) + + def is_floor_updated(self, updated_site, requested_site): + """ + Check if the floor details in a site have been updated. + + Args: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - updated_site (dict): The site details after the update. + - requested_site (dict): The site details as requested for the update. + Return: + bool: True if the floor details have been updated, False otherwise. + Description: + This method compares the floor details of the updated site with the requested site. + It checks if the name, rf_model, length, width, and height are equal, indicating + that the floor details have been updated. Returns True if the details match, and False otherwise. + """ + + keys_to_compare = ['length', 'width', 'height'] + if updated_site['name'] != requested_site['name'] or updated_site['rf_model'] != requested_site['rfModel']: + return False + + for key in keys_to_compare: + if not self.compare_float_values(updated_site[key], requested_site[key]): + return False + + return True + def site_requires_update(self): """ Check if the site requires updates. - Parameters: self (object): An instance of a class used for interacting with Cisco DNA Center. Returns: @@ -576,29 +667,19 @@ def site_requires_update(self): specified parameters, such as the site type and site details. """ - requested_site = self.want.get("site_params") - current_site = self.have.get("current_site") - - self.log("Current Site: " + str(current_site)) + type = self.have['current_site']['type'] + updated_site = self.have['current_site']['site'][type] + requested_site = self.want['site_params']['site'][type] + self.log("Current Site: " + str(updated_site)) self.log("Requested Site: " + str(requested_site)) - if requested_site.get('type') == "building": - requested_address = requested_site['site']['building']['address'] - current_address = current_site['site']['building']['address'] - - if requested_address is None or requested_address == current_address: - return False + if type == "building": + return not self.is_building_updated(updated_site, requested_site) - return True + elif type == "floor": + return not self.is_floor_updated(updated_site, requested_site) - obj_params = [ - ("type", "type"), - ("site", "site") - ] - - return any(not dnac_compare_equality(current_site.get(dnac_param), - requested_site.get(ansible_param)) - for (dnac_param, ansible_param) in obj_params) + return not self.is_area_updated(updated_site, requested_site) def get_have(self, config): """ @@ -610,9 +691,9 @@ def get_have(self, config): - self (object): An instance of a class used for interacting with Cisco DNA Center. Description: This method queries Cisco DNA Center to check if a specified site - exists. If the site exists, it retrieves details about the current - site, including the site ID and other relevant information. The - results are stored in the 'have' attribute for later reference. + exists. If the site exists, it retrieves details about the current + site, including the site ID and other relevant information. The + results are stored in the 'have' attribute for later reference. """ site_exists = False @@ -635,9 +716,7 @@ def get_have(self, config): def get_want(self, config): """ - Get all site-related information from the playbook needed for - creation in Cisco DNA Center. - + Get all site-related information from the playbook needed for creation/updation/deletion of site in Cisco DNA Center. Parameters: self (object): An instance of a class used for interacting with Cisco DNA Center. config (dict): A dictionary containing configuration information. @@ -664,11 +743,9 @@ def get_diff_merged(self, config): """ Update/Create site information in Cisco DNA Center with fields provided in the playbook. - Parameters: self (object): An instance of a class used for interacting with Cisco DNA Center. config (dict): A dictionary containing configuration information. - Returns: self (object): An instance of a class used for interacting with Cisco DNA Center. Description: @@ -700,8 +777,10 @@ def get_diff_merged(self, config): else: # Site does not neet update self.result['response'] = self.have.get("current_site") - self.result['msg'] = "Site - {0} does not need update".format(self.have.get("current_site")) - self.module.exit_json(**self.result) + self.msg = "Site - {0} does not need any update".format(self.have.get("current_site")) + self.log(self.msg) + self.result['msg'] = self.msg + return self else: # Creating New Site @@ -799,16 +878,10 @@ def delete_single_site(self, site_id, site_name): def get_diff_deleted(self, config): """ Call Cisco DNA Center API to delete sites with provided inputs. - Parameters: + - self (object): An instance of a class used for interacting with Cisco DNA Center. - config (dict): Dictionary containing information for site deletion. Returns: - If the deletion is successful, 'changed' is set to True, and the - 'response' includes execution details and the deleted site ID. If - an error occurs during the deletion, the method uses 'fail_json' to - raise an exception with the error message. If the site does not - exist, the method raises an exception with a message indicating that - the site was not found. - self: The result dictionary includes the following keys: - 'changed' (bool): Indicates whether changes were made during the deletion process. @@ -816,9 +889,8 @@ def get_diff_deleted(self, config): and the deleted site ID. - 'msg' (str): A message indicating the status of the deletion operation. Description: - This method initiates the deletion of a site by calling the - 'delete_site' function in the 'sites' family of the Cisco DNA - Center API. It uses the site ID obtained from the 'have' attribute. + This method initiates the deletion of a site by calling the 'delete_site' function in the 'sites' family + of the Cisco DNA Center API. It uses the site ID obtained from the 'have' attribute. """ site_exists = self.have.get("site_exists") @@ -860,6 +932,70 @@ def get_diff_deleted(self, config): return self + def verify_diff_merged(self, config): + """ + Verify the merged status(Creation/Updation) of site configuration in Cisco DNA Center. + Args: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + Description: + This method checks the merged status of a configuration in Cisco DNA Center by retrieving the current state + (have) and desired state (want) of the configuration, logs the states, and validates whether the specified + site exists in the DNA Center configuration. + """ + + self.get_have(config) + self.log(str(self.have)) + self.log(str(self.want)) + # Code to validate dnac config for merged state + site_exist = self.have.get("site_exists") + + if site_exist: + self.status = "success" + msg = "Requested Site - {0} present in Cisco DNA Center and creation verified.".format(self.want.get("site_name")) + self.log(msg) + require_update = self.site_requires_update() + + if not require_update: + self.log("Site - {0} Updation Verified Successfully.".format(self.want.get("site_name"))) + self. status = "success" + return self + + self.log("Playbook paramater doesnot match with the Cisco DNA Center means Merged task not executed successfully.") + + return self + + def verify_diff_deleted(self, config): + """ + Verify the deletion status of site configuration in Cisco DNA Center. + Args: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + Description: + This method checks the deletion status of a configuration in Cisco DNA Center. + It validates whether the specified site exists in the DNA Center configuration. + """ + + self.get_have(config) + self.log(str(self.have)) + self.log(str(self.want)) + # Code to validate dnac config for delete state + site_exist = self.have.get("site_exists") + + if not site_exist: + self.status = "success" + msg = "Requested Site - {0} already deleted from Cisco DNA Center and verified successfully.".format(self.want.get("site_name")) + self.log(msg) + return self + + self.log("Playbook paramater doesnot match with the Cisco DNA Center means Deletion not executed successfully.") + + return self + def main(): """ main entry point for module execution @@ -874,6 +1010,7 @@ def main(): 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log': {'type': 'bool', 'default': False}, 'validate_response_schema': {'type': 'bool', 'default': True}, + "config_verify": {"type": 'bool', "default": False}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, 'state': {'default': 'merged', 'choices': ['merged', 'deleted']} } @@ -890,12 +1027,15 @@ def main(): dnac_site.check_return_status() dnac_site.validate_input().check_return_status() + config_verify = dnac_site.params.get("config_verify") for config in dnac_site.validated_config: dnac_site.reset_values() dnac_site.get_want(config).check_return_status() dnac_site.get_have(config).check_return_status() dnac_site.get_diff_state_apply[state](config).check_return_status() + if config_verify: + dnac_site.verify_diff_state_apply[state](config).check_return_status() module.exit_json(**dnac_site.result) From a349969e5413abc2c2afe02265c6bb6f56cd2780 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 22 Dec 2023 17:26:42 +0530 Subject: [PATCH 18/63] remove unused import dnac_compare_equality --- plugins/modules/site_intent.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 9ec2559bae..84d7632bdd 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -307,7 +307,6 @@ validate_list_of_dicts, log, get_dict_result, - dnac_compare_equality, ) floor_plan = { From 4d49734180ceb26de7823891c88cb9e976b233a3 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 22 Dec 2023 17:37:56 +0530 Subject: [PATCH 19/63] add new line --- plugins/modules/site_intent.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 84d7632bdd..ea8d7c5849 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -955,6 +955,7 @@ def verify_diff_merged(self, config): self.status = "success" msg = "Requested Site - {0} present in Cisco DNA Center and creation verified.".format(self.want.get("site_name")) self.log(msg) + require_update = self.site_requires_update() if not require_update: From 03e1c4bc97d539c7dbae51036ed3cff13f0d9cbf Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 22 Dec 2023 12:21:16 +0000 Subject: [PATCH 20/63] PnP Reset and hostname added along with unprovision in provision intent module --- playbooks/PnP.yml | 18 +++---- playbooks/device_provision.yml | 9 +++- plugins/modules/discovery_intent.py | 32 ++++++------ plugins/modules/pnp_intent.py | 80 ++++++++++++++++++++++++++--- plugins/modules/provision_intent.py | 58 ++++++++++++++++++--- 5 files changed, 157 insertions(+), 40 deletions(-) diff --git a/playbooks/PnP.yml b/playbooks/PnP.yml index 0f3d864d57..6bf926d195 100644 --- a/playbooks/PnP.yml +++ b/playbooks/PnP.yml @@ -25,20 +25,18 @@ dnac_log: True state: merged config: - - device_info: - - serial_number: FKC2310E0HB - hostname: 1-5 + - site_name: Global/USA/San Francisco/BGL_18 + device_info: + - serial_number: CD2425L8M7 state: Unclaimed pid: c9300-24P is_sudi_required: False - - serial_number: FTC2320E0HB - hostname: 1-6 + - serial_number: FTC2320E0H9 state: Unclaimed pid: c9300-24P - serial_number: ETC2320E0HB - hostname: 1-7 state: Unclaimed pid: c9300-24P @@ -89,7 +87,7 @@ hostname: IAC-EWLC-Claimed device_info: - serial_number: FOX2639PAY7 - hostname: WLC + hostname: New_WLC state: Unclaimed pid: C9800-CL-K9 gateway: 204.192.101.1 @@ -105,6 +103,6 @@ state: deleted config: - device_info: - - serial_number: FKC2310E0HK - - serial_number: FTC2320E0HA - - serial_number: FKC2310E0HB + - serial_number: FTC2320E0HB #Will get deleted + - serial_number: FTC2320E0HA #Doesn't exist in the iobentory + - serial_number: FKC2310E0HB #Doesn't exist in the inventory diff --git a/playbooks/device_provision.yml b/playbooks/device_provision.yml index ed780ed5ab..fe3efe9190 100644 --- a/playbooks/device_provision.yml +++ b/playbooks/device_provision.yml @@ -27,4 +27,11 @@ - site_name: Global/USA/San Francisco/BGL_18 management_ip_address: 204.1.2.2 - \ No newline at end of file + + - name: Unprovision a wired device to a site + cisco.dnac.provision_intent: + <<: *dnac_login + dnac_log: True + state: deleted + config: + - management_ip_address: 204.1.2.2 diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index d1a22d800d..684b37f865 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -119,16 +119,16 @@ snmp_priv_protocol: description: SNMP privacy protocol (DES/AES128) type: str - snmp_ro_community: + snmp_r_o_community: description: Snmp RO community of the devices to be discovered type: str - snmp_ro_community_desc: + snmp_r_o_community_desc: description: Description for Snmp RO community type: str - snmp_rw_community: + snmp_r_w_community: description: Snmp RW community of the devices to be discovered type: str - snmp_rw_community_desc: + snmp_r_w_community_desc: description: Description for Snmp RW community type: str snmp_username: @@ -198,10 +198,10 @@ snmp_mode: string snmp_priv_passphrse: string snmp_priv_protocol: string - snmp_ro_community: string - snmp_ro_community_desc: string - snmp_rw_community: string - snmp_rw_community_desc: string + snmp_r_o_community: string + snmp_r_o_community_desc: string + snmp_r_w_community: string + snmp_r_w_community_desc: string snmp_username: string snmp_version: string timeout: integer @@ -331,10 +331,10 @@ def validate_input(self): 'snmp_mode': {'type': 'str', 'required': False}, 'snmp_priv_passphrase': {'type': 'str', 'required': False}, 'snmp_priv_protocol': {'type': 'str', 'required': False}, - 'snmp_ro_community': {'type': 'str', 'required': False}, - 'snmp_ro_community_desc': {'type': 'str', 'required': False}, - 'snmp_rw_community': {'type': 'str', 'required': False}, - 'snmp_rw_community_desc': {'type': 'str', 'required': False}, + 'snmp_r_o_community': {'type': 'str', 'required': False}, + 'snmp_r_o_community_desc': {'type': 'str', 'required': False}, + 'snmp_r_w_community': {'type': 'str', 'required': False}, + 'snmp_r_w_community_desc': {'type': 'str', 'required': False}, 'snmp_username': {'type': 'str', 'required': False}, 'snmp_version': {'type': 'str', 'required': True}, 'timeout': {'type': 'str', 'required': False}, @@ -498,13 +498,13 @@ def create_params(self, credential_ids=None, ip_address_list=None): new_object_params['snmpPrivProtocol'] = self.validated_config[0].get( 'snmp_priv_protocol') new_object_params['snmpROCommunity'] = self.validated_config[0].get( - 'snmp_ro_community') + 'snmp_r_o_community') new_object_params['snmpROCommunityDesc'] = self.validated_config[0].get( - 'snmp_ro_community_desc') + 'snmp_r_o_community_desc') new_object_params['snmpRWCommunity'] = self.validated_config[0].get( - 'snmp_rw_community') + 'snmp_r_w_community') new_object_params['snmpRWCommunityDesc'] = self.validated_config[0].get( - 'snmp_rw_community_desc') + 'snmp_r_w_community_desc') new_object_params['snmpUserName'] = self.validated_config[0].get( 'snmp_username') new_object_params['snmpVersion'] = self.validated_config[0].get('snmp_version') diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index bc72f39484..c9d27c65ec 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -16,6 +16,7 @@ - Manage operations add device, claim device and unclaim device of Onboarding Configuration(PnP) resource - API to add device to pnp inventory and claim it to a site. - API to delete device from the pnp inventory. +- API to reset the device from errored state. version_added: '6.6.0' extends_documentation_fragment: - cisco.dnac.intent_params @@ -89,7 +90,8 @@ elements: dict suboptions: hostname: - description: Pnp Device's hostname. + description: Pnp Device's hostname that we want to keep post claiming. Hostname can only + be changed during claiming not bulk adding/ single adding type: str state: description: Pnp Device's onbording state (Unclaimed/Claimed/Provisioned). @@ -115,6 +117,7 @@ device_onboarding_pnp.DeviceOnboardingPnp.delete_device_by_id_from_pnp, device_onboarding_pnp.DeviceOnboardingPnp.get_device_count, device_onboarding_pnp.DeviceOnboardingPnp.get_device_by_id, + device_onboarding_pnp.DeviceOnboardingPnp.update_device, sites.Sites.get_site, software_image_management_swim.SoftwareImageManagementSwim.get_software_image_details, configuration_templates.ConfigurationTemplates.gets_the_templates_available @@ -125,6 +128,7 @@ post /dna/intent/api/v1/onboarding/pnp-device/{id} get /dna/intent/api/v1/onboarding/pnp-device/count get /dna/intent/api/v1/onboarding/pnp-device + put /onboarding/pnp-device/${id} get /dna/intent/api/v1/site get /dna/intent/api/v1/image/importation get /dna/intent/api/v1/template-programmer/template @@ -372,6 +376,7 @@ def get_pnp_params(self, params): and stores it for further processing and calling the parameters in other APIs. """ + params_list = params["device_info"] device_info_list = [] for param in params_list: @@ -471,6 +476,32 @@ def get_claim_params(self): return claim_params + def get_reset_params(self): + + reset_params = { + "deviceResetList": [ + { + "configList": [ + { + "configId": self.have.get('template_id'), + "configParameters": [ + { + "key": "", + "value": "" + } + ] + } + ], + "deviceId": self.have.get('device_id'), + "licenseLevel": "", + "licenseType": "", + "topOfStackSerialNumber": "" + } + ] + } + + return reset_params + def get_have(self): """ Get the current image, template and site details from the DNAC. @@ -539,7 +570,7 @@ def get_have(self): # check if given site exits, if exists store current site info site_exists = False if not isinstance(self.want.get("site_name"), str) and \ - not self.want.get('pnp_params').get('deviceInfo'): + not self.want.get('pnp_params')[0].get('deviceInfo'): self.msg = "Name of the site must be a string" self.status = "failed" return self @@ -586,7 +617,7 @@ def get_have(self): return self else: - if not self.want.get('pnp_params').get('deviceInfo'): + if not self.want.get('pnp_params')[0].get('deviceInfo'): self.msg = "Either Site Name or Device details must be added" self.status = "failed" return self @@ -595,7 +626,6 @@ def get_have(self): parameters from dnac for comparison" self.status = "success" self.have = have - return self def get_want(self, config): @@ -648,6 +678,7 @@ def get_want(self, config): self.msg = "Successfully collected all parameters from playbook " + \ "for comparison" self.status = "success" + # return self def get_diff_merged(self): @@ -672,7 +703,7 @@ class instance for further use. self.status = "failed" return self - if len(self.want.get("pnp_params")) >= 2: + if len(self.want.get("pnp_params")) > 1: devices_added = [] for device in self.want.get("pnp_params"): multi_device_response = self.dnac_apply['exec']( @@ -683,6 +714,7 @@ class instance for further use. if (multi_device_response and (len(multi_device_response) == 1)): devices_added.append(device) + if (len(self.want.get("pnp_params")) - len(devices_added)) == 0: self.result['response'] = [] self.result['msg'] = "Devices are already added" @@ -793,16 +825,50 @@ class instance for further use. params=planned_count_params, ) + dev_details_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="get_device_by_id", + params={"id": self.have["device_id"]} + ) + + pnp_state = dev_details_response.get("deviceInfo").get("state") + if not self.want["site_name"]: self.result['response'] = self.have.get("device_found") self.result['msg'] = "Device is already added" else: + update_payload = {"deviceInfo": self.want.get('pnp_params')[0].get("deviceInfo")} + update_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="update_device", + params={"id": self.have["device_id"], + "payload": update_payload}, + op_modifies=True, + ) + self.log(str(update_response)) + + if pnp_state == "Error": + reset_paramters = self.get_reset_params() + reset_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="update_device", + params={"payload": reset_paramters}, + op_modifies=True, + ) + self.log(str(reset_response)) + self.result['msg'] = "Device reset done Successfully" + self.result['response'] = reset_response + self.result['diff'] = self.validated_config + self.result['changed'] = True + if ( prov_dev_response.get("response") == 0 and - plan_dev_response.get("response") == 0 + plan_dev_response.get("response") == 0 and + pnp_state == "Unclaimed" ): claim_params = self.get_claim_params() self.log(str(claim_params)) + claim_response = self.dnac_apply['exec']( family="device_onboarding_pnp", function='claim_a_device_to_a_site', @@ -818,6 +884,8 @@ class instance for further use. else: self.result['response'] = self.have.get("device_found") self.result['msg'] = "Device is already claimed" + if update_response.get("deviceInfo"): + self.result['changed'] = True return self diff --git a/plugins/modules/provision_intent.py b/plugins/modules/provision_intent.py index 690b1522e8..f32effe5f7 100644 --- a/plugins/modules/provision_intent.py +++ b/plugins/modules/provision_intent.py @@ -332,7 +332,6 @@ def get_site_type(self, site_name=None): return site_type def get_wired_params(self): - """ Prepares the payload for provisioning of the wired devices @@ -349,6 +348,7 @@ def get_wired_params(self): paramters and stores it for further processing and calling the parameters in other APIs. """ + wired_params = { "deviceManagementIpAddress": self.validated_config[0]["management_ip_address"], "siteNameHierarchy": self.validated_config[0].get("site_name") @@ -357,7 +357,6 @@ def get_wired_params(self): return wired_params def get_wireless_params(self): - """ Prepares the payload for provisioning of the wireless devices @@ -375,6 +374,7 @@ def get_wireless_params(self): paramters and stores it for further processing and calling the parameters in other APIs. """ + wireless_params = [ { "site": self.validated_config[0].get("site_name"), @@ -406,7 +406,6 @@ def get_wireless_params(self): return wireless_params def get_want(self): - """ Get all provision related informantion from the playbook Args: @@ -422,6 +421,7 @@ def get_want(self): It stores all the paramters passed from the playbook for further processing before calling the APIs """ + self.want = {} self.want["device_type"] = self.get_dev_type() if self.want["device_type"] == "wired": @@ -437,7 +437,6 @@ def get_want(self): return self def get_diff_merged(self): - """ Add to provision database Args: @@ -451,6 +450,7 @@ def get_diff_merged(self): Cisco DNA Center. The updated results and status are stored in the class instance for further use. """ + device_type = self.want.get("device_type") if device_type == "wired": try: @@ -470,7 +470,7 @@ class instance for further use. status = status_response.get("status") if status == "success": - response = response = self.dnac_apply['exec']( + response = self.dnac_apply['exec']( family="sda", function="re_provision_wired_device", op_modifies=True, @@ -495,6 +495,7 @@ class instance for further use. else: self.result['msg'] = "Passed device is neither wired nor wireless" self.result['response'] = self.want["prov_params"] + return self task_id = response.get("taskId") provision_info = self.get_task_status(task_id=task_id) @@ -506,7 +507,6 @@ class instance for further use. return self def get_diff_deleted(self): - """ Delete from provision database Args: @@ -519,7 +519,51 @@ def get_diff_deleted(self): raise Exception if any error occured. """ - pass + device_type = self.want.get("device_type") + + if device_type != "wired": + self.result['msg'] = "APIs are not supported for the device" + return self + + try: + status_response = self.dnac_apply['exec']( + family="sda", + function="get_provisioned_wired_device", + op_modifies=True, + params={ + "device_management_\ + ip_address": + self.validated_config[0]["management_ip_address"] + }, + ) + + except Exception: + status_response = {} + + status = status_response.get("status") + + if status == "success": + response = self.dnac_apply['exec']( + family="sda", + function="delete_provisioned_wired_device", + op_modifies=True, + params={"device_management_\ + ip_address": + self.validated_config[0]["management_ip_address"] + }, + ) + + else: + self.result['msg'] = "Passed IP address is not provisioned" + self.result['response'] = self.want["prov_params"] + return self + + task_id = response.get("taskId") + deletion_info = self.get_task_status(task_id=task_id) + self.result["changed"] = True + self.result['msg'] = "Deletion done Successfully" + self.result['diff'] = self.validated_config + self.result['response'] = task_id return self From 2a45e73185188a2cb9d39b35fac06dc1704a0880 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 22 Dec 2023 12:30:41 +0000 Subject: [PATCH 21/63] PnP Reset and hostname added along with unprovision in provision intent module --- plugins/modules/pnp_intent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index c9d27c65ec..62ab00b10e 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -91,7 +91,7 @@ suboptions: hostname: description: Pnp Device's hostname that we want to keep post claiming. Hostname can only - be changed during claiming not bulk adding/ single adding + be changed during claiming not bulk adding/ single adding type: str state: description: Pnp Device's onbording state (Unclaimed/Claimed/Provisioned). From 31a60bc831122b30cd999e2646008de87ebf3a81 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 22 Dec 2023 12:35:00 +0000 Subject: [PATCH 22/63] PnP Reset and hostname added along with unprovision in provision intent module --- plugins/modules/provision_intent.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/modules/provision_intent.py b/plugins/modules/provision_intent.py index f32effe5f7..8592228868 100644 --- a/plugins/modules/provision_intent.py +++ b/plugins/modules/provision_intent.py @@ -547,7 +547,8 @@ def get_diff_deleted(self): family="sda", function="delete_provisioned_wired_device", op_modifies=True, - params={"device_management_\ + params={ + "device_management_\ ip_address": self.validated_config[0]["management_ip_address"] }, From e851c38d9655ef72ea4df173ab12a44b67ffe6f5 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 22 Dec 2023 14:19:01 +0000 Subject: [PATCH 23/63] PnP Reset and hostname added along with unprovision in provision intent module --- plugins/modules/discovery_intent.py | 32 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 684b37f865..d1a22d800d 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -119,16 +119,16 @@ snmp_priv_protocol: description: SNMP privacy protocol (DES/AES128) type: str - snmp_r_o_community: + snmp_ro_community: description: Snmp RO community of the devices to be discovered type: str - snmp_r_o_community_desc: + snmp_ro_community_desc: description: Description for Snmp RO community type: str - snmp_r_w_community: + snmp_rw_community: description: Snmp RW community of the devices to be discovered type: str - snmp_r_w_community_desc: + snmp_rw_community_desc: description: Description for Snmp RW community type: str snmp_username: @@ -198,10 +198,10 @@ snmp_mode: string snmp_priv_passphrse: string snmp_priv_protocol: string - snmp_r_o_community: string - snmp_r_o_community_desc: string - snmp_r_w_community: string - snmp_r_w_community_desc: string + snmp_ro_community: string + snmp_ro_community_desc: string + snmp_rw_community: string + snmp_rw_community_desc: string snmp_username: string snmp_version: string timeout: integer @@ -331,10 +331,10 @@ def validate_input(self): 'snmp_mode': {'type': 'str', 'required': False}, 'snmp_priv_passphrase': {'type': 'str', 'required': False}, 'snmp_priv_protocol': {'type': 'str', 'required': False}, - 'snmp_r_o_community': {'type': 'str', 'required': False}, - 'snmp_r_o_community_desc': {'type': 'str', 'required': False}, - 'snmp_r_w_community': {'type': 'str', 'required': False}, - 'snmp_r_w_community_desc': {'type': 'str', 'required': False}, + 'snmp_ro_community': {'type': 'str', 'required': False}, + 'snmp_ro_community_desc': {'type': 'str', 'required': False}, + 'snmp_rw_community': {'type': 'str', 'required': False}, + 'snmp_rw_community_desc': {'type': 'str', 'required': False}, 'snmp_username': {'type': 'str', 'required': False}, 'snmp_version': {'type': 'str', 'required': True}, 'timeout': {'type': 'str', 'required': False}, @@ -498,13 +498,13 @@ def create_params(self, credential_ids=None, ip_address_list=None): new_object_params['snmpPrivProtocol'] = self.validated_config[0].get( 'snmp_priv_protocol') new_object_params['snmpROCommunity'] = self.validated_config[0].get( - 'snmp_r_o_community') + 'snmp_ro_community') new_object_params['snmpROCommunityDesc'] = self.validated_config[0].get( - 'snmp_r_o_community_desc') + 'snmp_ro_community_desc') new_object_params['snmpRWCommunity'] = self.validated_config[0].get( - 'snmp_r_w_community') + 'snmp_rw_community') new_object_params['snmpRWCommunityDesc'] = self.validated_config[0].get( - 'snmp_r_w_community_desc') + 'snmp_rw_community_desc') new_object_params['snmpUserName'] = self.validated_config[0].get( 'snmp_username') new_object_params['snmpVersion'] = self.validated_config[0].get('snmp_version') From 7c8f2125af221fed1f7423b88b40a0700f6b0ba2 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 22 Dec 2023 14:25:08 +0000 Subject: [PATCH 24/63] PnP Reset and hostname added along with unprovision in provision intent module --- playbooks/PnP.yml | 2 +- plugins/modules/provision_intent.py | 25 ++++++++++++------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/playbooks/PnP.yml b/playbooks/PnP.yml index 6bf926d195..295bec57c7 100644 --- a/playbooks/PnP.yml +++ b/playbooks/PnP.yml @@ -104,5 +104,5 @@ config: - device_info: - serial_number: FTC2320E0HB #Will get deleted - - serial_number: FTC2320E0HA #Doesn't exist in the iobentory + - serial_number: FTC2320E0HA #Doesn't exist in the inventory - serial_number: FKC2310E0HB #Doesn't exist in the inventory diff --git a/plugins/modules/provision_intent.py b/plugins/modules/provision_intent.py index 8592228868..c055eb1c08 100644 --- a/plugins/modules/provision_intent.py +++ b/plugins/modules/provision_intent.py @@ -542,23 +542,22 @@ def get_diff_deleted(self): status = status_response.get("status") - if status == "success": - response = self.dnac_apply['exec']( - family="sda", - function="delete_provisioned_wired_device", - op_modifies=True, - params={ - "device_management_\ - ip_address": - self.validated_config[0]["management_ip_address"] - }, - ) - - else: + if status != "success": self.result['msg'] = "Passed IP address is not provisioned" self.result['response'] = self.want["prov_params"] return self + response = self.dnac_apply['exec']( + family="sda", + function="delete_provisioned_wired_device", + op_modifies=True, + params={ + "device_management_\ + ip_address": + self.validated_config[0]["management_ip_address"] + }, + ) + task_id = response.get("taskId") deletion_info = self.get_task_status(task_id=task_id) self.result["changed"] = True From 1874627d4a23d8d2718232908937e444a1419c27 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 22 Dec 2023 14:55:18 +0000 Subject: [PATCH 25/63] PnP Reset and hostname added along with unprovision in provision intent module --- plugins/modules/pnp_intent.py | 143 +++++++++++++++++----------------- 1 file changed, 72 insertions(+), 71 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 62ab00b10e..1305290ef8 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -678,7 +678,7 @@ def get_want(self, config): self.msg = "Successfully collected all parameters from playbook " + \ "for comparison" self.status = "success" - # + return self def get_diff_merged(self): @@ -811,83 +811,84 @@ class instance for further use. self.status = "failed" return self - else: - prov_dev_response = self.dnac_apply['exec']( - family="device_onboarding_pnp", - function='get_device_count', - op_modifies=True, - params=provisioned_count_params, - ) - plan_dev_response = self.dnac_apply['exec']( - family="device_onboarding_pnp", - function='get_device_count', - op_modifies=True, - params=planned_count_params, - ) + prov_dev_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_count', + op_modifies=True, + params=provisioned_count_params, + ) + plan_dev_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_count', + op_modifies=True, + params=planned_count_params, + ) + + dev_details_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="get_device_by_id", + params={"id": self.have["device_id"]} + ) + + pnp_state = dev_details_response.get("deviceInfo").get("state") + + if not self.want["site_name"]: + self.result['response'] = self.have.get("device_found") + self.result['msg'] = "Device is already added" + return self + + update_payload = {"deviceInfo": self.want.get('pnp_params')[0].get("deviceInfo")} + update_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="update_device", + params={"id": self.have["device_id"], + "payload": update_payload}, + op_modifies=True, + ) + self.log(str(update_response)) - dev_details_response = self.dnac_apply['exec']( + if pnp_state == "Error": + reset_paramters = self.get_reset_params() + reset_response = self.dnac_apply['exec']( family="device_onboarding_pnp", - function="get_device_by_id", - params={"id": self.have["device_id"]} + function="update_device", + params={"payload": reset_paramters}, + op_modifies=True, ) + self.log(str(reset_response)) + self.result['msg'] = "Device reset done Successfully" + self.result['response'] = reset_response + self.result['diff'] = self.validated_config + self.result['changed'] = True - pnp_state = dev_details_response.get("deviceInfo").get("state") + if not ( + prov_dev_response.get("response") == 0 and + plan_dev_response.get("response") == 0 and + pnp_state == "Unclaimed" + ): + self.result['response'] = self.have.get("device_found") + self.result['msg'] = "Device is already claimed" + if update_response.get("deviceInfo"): + self.result['changed'] = True + return self - if not self.want["site_name"]: - self.result['response'] = self.have.get("device_found") - self.result['msg'] = "Device is already added" - else: - update_payload = {"deviceInfo": self.want.get('pnp_params')[0].get("deviceInfo")} - update_response = self.dnac_apply['exec']( - family="device_onboarding_pnp", - function="update_device", - params={"id": self.have["device_id"], - "payload": update_payload}, - op_modifies=True, - ) - self.log(str(update_response)) - - if pnp_state == "Error": - reset_paramters = self.get_reset_params() - reset_response = self.dnac_apply['exec']( - family="device_onboarding_pnp", - function="update_device", - params={"payload": reset_paramters}, - op_modifies=True, - ) - self.log(str(reset_response)) - self.result['msg'] = "Device reset done Successfully" - self.result['response'] = reset_response - self.result['diff'] = self.validated_config - self.result['changed'] = True + claim_params = self.get_claim_params() + self.log(str(claim_params)) - if ( - prov_dev_response.get("response") == 0 and - plan_dev_response.get("response") == 0 and - pnp_state == "Unclaimed" - ): - claim_params = self.get_claim_params() - self.log(str(claim_params)) - - claim_response = self.dnac_apply['exec']( - family="device_onboarding_pnp", - function='claim_a_device_to_a_site', - op_modifies=True, - params=claim_params, - ) - self.log(str(claim_response)) - if claim_response.get("response") == "Device Claimed": - self.result['msg'] = "Only Device Claimed Successfully" - self.result['response'] = claim_response - self.result['diff'] = self.validated_config - self.result['changed'] = True - else: - self.result['response'] = self.have.get("device_found") - self.result['msg'] = "Device is already claimed" - if update_response.get("deviceInfo"): - self.result['changed'] = True + claim_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='claim_a_device_to_a_site', + op_modifies=True, + params=claim_params, + ) + self.log(str(claim_response)) + if claim_response.get("response") == "Device Claimed": + self.result['msg'] = "Only Device Claimed Successfully" + self.result['response'] = claim_response + self.result['diff'] = self.validated_config + self.result['changed'] = True - return self + return self def get_diff_deleted(self): """ From 54a993641af1da1f6987a2f0e788207a3bbdd1a1 Mon Sep 17 00:00:00 2001 From: Madhan Date: Fri, 22 Dec 2023 23:44:13 +0530 Subject: [PATCH 26/63] Changes in intent modules --- changelogs/changelog.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index d16f20c382..301299bea4 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -763,3 +763,10 @@ releases: - Changes in site intent module to support one-shot site deletion - To Support provisioning wired device, reboot AP's, export device list, delete provisioned devices. - Change the variable names into snake case in all the intent modules for better readability. + 6.10.1: + release_date: "2023-12-22" + changes: + release_summary: Changes in network settings, site, inventory and provisioning intent modules + minor_changes: + - Introducing config_verify to verify the state operations in Catalyst Center in network settings and site intent module + - Changes to support inventory and provisioning intent modules From 9bfc6b35dc5bd19effa82a9dcc017ccf858c514c Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 11 Jan 2024 15:04:09 +0530 Subject: [PATCH 27/63] added verification for device addition/deletion, updation of interface details, device roles, UDF creation/deletion, address Jira tickets, add support for encryption protocols --- plugins/modules/inventory_intent.py | 750 ++++++++++++++++++++++------ 1 file changed, 588 insertions(+), 162 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 117c687410..b63c4ff0dc 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -24,6 +24,10 @@ author: Abhishek Maheshwari (@abmahesh) Madhan Sankaranarayanan (@madhansansel) options: + config_verify: + description: Set to True to verify the Cisco DNA Center config after applying the playbook config. + type: bool + default: False state: description: The state of Cisco DNA Center after module completion. type: str @@ -59,17 +63,10 @@ http_username: description: Device's http username. Required for Adding Compute,Firepower Management Devices. type: str - id: - description: Id path parameter that is Device ID. - type: str ip_address: description: Device's ipAddress. Required for Adding/Updating/Deleting/Resyncing Device except Meraki Devices. elements: str type: list - meraki_org_id: - description: Device's meraki org id. - elements: str - type: list netconf_port: description: Device's netconf port. type: str @@ -100,7 +97,7 @@ snmp_priv_protocol: description: Device's snmp Private Protocol. Required for Adding Network, Compute, Third Party Devices. type: str - default: "AES128" + default: "CISCOAES128" snmp_ro_community: description: Device's snmp ROCommunity. Required for Adding V2C Devices. type: str @@ -143,6 +140,26 @@ description: If forcesync is true then device sync would run in high priority thread if available, else the sync will fail. type: bool default: false + device_added: + description: Make this as true needed for the addition of device in inventory. + type: bool + default: false + device_updated: + description: Make this as true needed for the updation of device role, interface details, device credentails or details. + type: bool + default: false + device_resync: + description: Make this as true needed for the resyncing of device. + type: bool + default: false + reboot_device: + description: Make this as true needed for the Rebooting of Access Points. + type: bool + default: false + credential_update: + description: Make this as true needed for the updation of device credentials and other device details. + type: bool + default: false clean_config: description: Required if need to delete the Provisioned device by clearing current configuration. type: bool @@ -242,7 +259,7 @@ """ EXAMPLES = r""" -- name: Add/Update new device in Inventory with full credentials +- name: Add new device in Inventory with full credentials cisco.dnac.inventory_intent: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" @@ -255,17 +272,15 @@ state: merged config: - cli_transport: string - compute_device: true + compute_device: false enable_password: string extended_discovery_info: string http_password: string http_port: string - http_secure: true + http_secure: false http_username: string ip_address: - string - meraki_org_id: - - string netconf_port: string password: string serial_number: string @@ -282,9 +297,6 @@ snmp_version: string type: string device_added: true - update_mgmt_ipaddresslist: - - exist_mgmt_ipaddress: string - new_mgmt_ipaddress: string username: string - name: Add new Compute device in Inventory with full credentials.Inputs needed for Compute Device @@ -312,6 +324,7 @@ snmp_retry: 3 snmp_timeout: 5 snmp_username: string + compute_device: true username: string device_added: true type: "COMPUTE_DEVICE" @@ -377,6 +390,46 @@ device_added: true type: "THIRD_PARTY_DEVICE" +- name: Update device details or credentails in Inventory + cisco.dnac.inventory_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: False + state: merged + config: + - cli_transport: string + compute_device: false + password: string + enable_password: string + extended_discovery_info: string + http_password: string + http_port: string + http_secure: false + http_username: string + ip_address: + - string + netconf_port: string + serial_number: string + snmp_auth_passphrase: string + snmp_auth_protocol: string + snmp_mode: string + snmp_priv_passphrase: string + snmp_priv_protocol: string + snmp_username: string + snmp_version: string + type: string + device_update: true + credential_update: true + update_mgmt_ipaddresslist: + - exist_mgmt_ipaddress: string + new_mgmt_ipaddress: string + username: string + - name: Associate Wired Devices to site and Provisioned it in Inventory cisco.dnac.inventory_intent: dnac_host: "{{dnac_host}}" @@ -626,44 +679,75 @@ def validate_input(self): 'self.msg' will describe the validation issues. """ - temp_spec = {'cli_transport': {'default': "telnet", 'type': 'str'}, - 'compute_device': {'type': 'bool'}, - 'enable_password': {'type': 'str'}, - 'extended_discovery_info': {'type': 'str'}, - 'http_password': {'type': 'str'}, - 'http_port': {'type': 'str'}, - 'http_secure': {'type': 'bool'}, - 'http_username': {'type': 'str'}, - 'ip_address': {'type': 'list', 'elements': 'str'}, - 'meraki_org_id': {'type': 'list', 'elements': 'str'}, - 'netconf_port': {'type': 'str'}, - 'password': {'type': 'str'}, - 'serial_number': {'type': 'str'}, - 'snmp_auth_passphrase': {'type': 'str'}, - 'snmp_auth_protocol': {'default': "SHA", 'type': 'str'}, - 'snmp_mode': {'default': "AUTHPRIV", 'type': 'str'}, - 'snmp_priv_passphrase': {'type': 'str'}, - 'snmp_priv_protocol': {'default': "AES128", 'type': 'str'}, - 'snmp_ro_community': {'default': "public", 'type': 'str'}, - 'snmp_rw_community': {'default': "private", 'type': 'str'}, - 'snmp_retry': {'default': 3, 'type': 'int'}, - 'snmp_timeout': {'default': 5, 'type': 'int'}, - 'snmp_username': {'type': 'str'}, - 'snmp_version': {'default': "v3", 'type': 'str'}, - 'update_mgmt_ipaddresslist': {'type': 'list', 'elements': 'dict'}, - 'username': {'type': 'str'}, - 'update_device_role': {'type': 'dict'}, - 'device_added': {'type': 'bool'}, - 'device_resync': {'type': 'bool'}, - 'force_sync': {'type': 'bool'}, - 'clean_config': {'type': 'bool'}, - 'add_user_defined_field': {'type': 'dict'}, - 'upate_interface_details': {'type': 'dict'}, - 'deployment_mode': {'default': 'Deploy', 'type': 'str'}, - 'provision_wired_device': {'type': 'dict'}, - 'provision_wireless_device': {'type': 'list', 'elements': 'dict'}, - 'export_device_list': {'type': 'dict'} - } + temp_spec = { + 'cli_transport': {'default': "telnet", 'type': 'str'}, + 'compute_device': {'type': 'bool'}, + 'enable_password': {'type': 'str'}, + 'extended_discovery_info': {'type': 'str'}, + 'http_password': {'type': 'str'}, + 'http_port': {'type': 'str'}, + 'http_secure': {'type': 'bool'}, + 'http_username': {'type': 'str'}, + 'ip_address': {'type': 'list', 'elements': 'str'}, + 'netconf_port': {'type': 'str'}, + 'password': {'type': 'str'}, + 'serial_number': {'type': 'str'}, + 'snmp_auth_passphrase': {'type': 'str'}, + 'snmp_auth_protocol': {'default': "SHA", 'type': 'str'}, + 'snmp_mode': {'default': "AUTHPRIV", 'type': 'str'}, + 'snmp_priv_passphrase': {'type': 'str'}, + 'snmp_priv_protocol': {'default': "CISCOAES128", 'type': 'str'}, + 'snmp_ro_community': {'default': "public", 'type': 'str'}, + 'snmp_rw_community': {'default': "private", 'type': 'str'}, + 'snmp_retry': {'default': 3, 'type': 'int'}, + 'snmp_timeout': {'default': 5, 'type': 'int'}, + 'snmp_username': {'type': 'str'}, + 'snmp_version': {'default': "v3", 'type': 'str'}, + 'update_mgmt_ipaddresslist': {'type': 'list', 'elements': 'dict'}, + 'username': {'type': 'str'}, + 'update_device_role': {'type': 'dict'}, + 'device_added': {'type': 'bool'}, + 'device_updated': {'type': 'bool'}, + 'device_resync': {'type': 'bool'}, + 'reboot_device': {'type': 'bool'}, + 'credential_update': {'type': 'bool'}, + 'force_sync': {'type': 'bool'}, + 'clean_config': {'type': 'bool'}, + 'add_user_defined_field': { + 'type': 'dict', + 'name': {'type': 'str'}, + 'description': {'type': 'str'}, + 'value': {'type': 'str'}, + }, + 'update_interface_details': { + 'type': 'dict', + 'description': {'type': 'str'}, + 'vlan_id': {'type': 'int'}, + 'voice_vlan_id': {'type': 'int'}, + }, + 'export_device_list': { + 'type': 'dict', + 'password': {'type': 'str'}, + 'operation_enum': {'type': 'str'}, + 'parameters': {'type': 'str'}, + }, + 'deployment_mode': {'default': 'Deploy', 'type': 'str'}, + 'provision_wired_device': {'type': 'dict'}, + 'provision_wireless_device': { + 'type': 'list', + 'site_name': {'type': 'str'}, + 'managed_ap_locations': {'type': 'list', 'elements': 'str'}, + 'dynamic_interfaces': { + 'type': 'list', + 'interface_ip_address': {'type': 'str'}, + 'interface_netmask_in_cidr': {'type': 'int'}, + 'interface_gateway': {'type': 'str'}, + 'lag_or_port_number': {'type': 'int'}, + 'vlan_id': {'type': 'int'}, + 'interface_name': {'type': 'str'}, + }, + } + } # Validate device params valid_temp, invalid_params = validate_list_of_dicts( @@ -678,7 +762,7 @@ def validate_input(self): return self self.validated_config = valid_temp - log(str(valid_temp)) + self.log(str(valid_temp)) self.msg = "Successfully validated input" self.status = "success" @@ -824,6 +908,104 @@ def add_field_to_devices(self, device_ids): return self + def trigger_export_api(self, payload_params): + """ + Triggers the export API to generate a CSV file containing device details based on the given payload parameters. + Args: + self (object): An instance of a class used for interacting with Cisco DNA Center. + payload_params (dict): A dictionary containing parameters required for the export API. + Returns: + dict: The response from the export API, including information about the task and file ID. + If the export is successful, the CSV file can be downloaded using the file ID. + Description: + The function initiates the export API in Cisco DNA Center to generate a CSV file containing detailed information + about devices.The response from the API includes task details and a file ID. + + """ + response = self.dnac._exec( + family="devices", + function='export_device_list', + op_modifies=True, + params=payload_params, + ) + self.log(str(response)) + response = response.get("response") + task_id = response.get("taskId") + while True: + execution_details = self.get_task_details(task_id) + + if execution_details.get("additionalStatusURL"): + file_id = execution_details.get("additionalStatusURL").split("/")[-1] + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Could not get the File ID because of {0} so can't export device details in csv file".format(failure_reason) + else: + self.msg = "Could not get the File ID so can't export device details in csv file" + self.log(self.msg) + self.check_return_status() + + # With this File ID call the Download File by FileID API and process the response + response = self.dnac._exec( + family="file", + function='download_a_file_by_fileid', + op_modifies=True, + params={"file_id": file_id}, + ) + + return response + + def decrypt_and_read_csv(self, response, password): + """ + Args: + self (object): An instance of a class used for interacting with Cisco DNA Center. + response (requests.Response): HTTP response object containing the encrypted CSV file. + password (str): Password used for decrypting the CSV file. + Returns: + csv.DictReader: A CSV reader object for the decrypted content, allowing iteration over rows as dictionaries. + Description: + Decrypts and reads a CSV-like file from the given HTTP response using the provided password. + """ + + zip_data = BytesIO(response.data) + + if not HAS_PYZIPPER: + self.msg = "pyzipper is required for this module. Install pyzipper to use this functionality." + self.log(self.msg) + self.status = "failed" + + return self + + snmp_protocol = self.config[0].get('snmp_priv_protocol', 'CISCOAES128') + encryption_dict = { + 'CISCOAES128': 'pyzipper.WZ_AES128', + 'CISCOAES192': 'pyzipper.WZ_AES192', + 'CISCOAES256': 'pyzipper.WZ_AES' + } + encryption_method = encryption_dict[snmp_protocol] + + # Create a PyZipper object with the password + with pyzipper.AESZipFile(zip_data, 'r', compression=pyzipper.ZIP_LZMA, encryption=encryption_method) as zip_ref: + # Assuming there is a single file in the zip archive + file_name = zip_ref.namelist()[0] + + # Extract the content of the file with the provided password + file_content_binary = zip_ref.read(file_name, pwd=password.encode('utf-8')) + + # Now 'file_content_binary' contains the binary content of the decrypted file + # Since the content is text, so we can decode it + file_content_text = file_content_binary.decode('utf-8') + + # Now 'file_content_text' contains the text content of the decrypted file + self.log(file_content_text) + + # Parse the CSV-like string into a list of dictionaries + csv_reader = csv.DictReader(StringIO(file_content_text)) + + return csv_reader + def export_device_details(self): """ Export device details from Cisco DNA Center into a CSV file. @@ -863,7 +1045,6 @@ def export_device_details(self): if not self.is_valid_password(password): self.status = "failed" - self.result['changed'] = False detailed_msg = """Invalid password. Min password length is 8 and it should contain atleast one lower case letter, one uppercase letter, one digit and one special characters from -=\\;,./~!@#$%^&*()_+{}[]|:?""" formatted_msg = ' '.join(line.strip() for line in detailed_msg.splitlines()) @@ -878,71 +1059,12 @@ def export_device_details(self): "paramters": export_device_list.get("paramters") } - response = self.dnac._exec( - family="devices", - function='export_device_list', - op_modifies=True, - params=payload_params, - ) - self.log(str(response)) - response = response.get("response") - task_id = response.get("taskId") - - while True: - execution_details = self.get_task_details(task_id) - - if execution_details.get("additionalStatusURL"): - file_id = execution_details.get("additionalStatusURL").split("/")[-1] - break - elif execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Could not get the File ID because of {0} so can't export device details in csv file".format(failure_reason) - else: - self.msg = "Could not get the File ID so can't export device details in csv file" - self.log(self.msg) - - return self - - # With this File ID call the Download File by FileID API and process the response - response = self.dnac._exec( - family="file", - function='download_a_file_by_fileid', - op_modifies=True, - params={"file_id": file_id}, - ) + response = self.trigger_export_api(payload_params) if payload_params["operationEnum"] == "0": - zip_data = BytesIO(response.data) - - if not HAS_PYZIPPER: - self.msg = "pyzipper is required for this module. Install pyzipper to use this functionality." - self.log(self.msg) - self.status = "failed" - - return self - - # Create a PyZipper object with the password - with pyzipper.AESZipFile(zip_data, 'r', compression=pyzipper.ZIP_LZMA, encryption=pyzipper.WZ_AES) as zip_ref: - # Assuming there is a single file in the zip archive - file_name = zip_ref.namelist()[0] - - # Extract the content of the file with the provided password - file_content_binary = zip_ref.read(file_name, pwd=password.encode('utf-8')) - - # Now 'file_content_binary' contains the binary content of the decrypted file - # Since the content is text, so we can decode it - file_content_text = file_content_binary.decode('utf-8') - - # Now 'file_content_text' contains the text content of the decrypted file - self.log(file_content_text) - - # Parse the CSV-like string into a list of dictionaries - csv_reader = csv.DictReader(StringIO(file_content_text)) temp_file_name = response.filename output_file_name = temp_file_name.split(".")[0] + ".csv" - + csv_reader = self.decrypt_and_read_csv(response, password) else: encoded_resp = response.data.decode(encoding='utf-8') self.log(str(encoded_resp)) @@ -1003,7 +1125,7 @@ def resync_devices(self): device_ids = self.get_device_ids(device_ips) try: - force_sync = self.config[0].get("force_sync", "False") + force_sync = self.config[0].get("force_sync", False) resync_param_dict = { 'payload': device_ids, 'force_sync': force_sync @@ -1229,7 +1351,8 @@ def handle_all_failed_provision(self, device_type): """ self.status = "failed" - self.log("{0} Device Provisioning failed for all devices".format(device_type)) + self.msg = "{0} Device Provisioning failed for all devices".format(device_type) + self.log(self.msg) def handle_partially_provisioned(self, provision_count, device_type): """ @@ -1281,6 +1404,13 @@ def provisioned_wired_device(self): try: provision_wired_params['deviceManagementIpAddress'] = device_ip + # Check till device comes into managed state + while True: + response = self.get_device_response(device_ip) + self.log("Device is in {0} state waiting for Managed State.".format(response['managementState'])) + if response['managementState'] == "Managed": + break + response = self.dnac._exec( family="sda", function='provision_wired_device', @@ -1457,6 +1587,13 @@ def provisioned_wireless_devices(self, device_ips): self.get_wireless_param(device_ip).check_return_status() provisioning_params = self.wireless_param + # Check till device comes into managed state + while True: + response = self.get_device_response(device_ip) + self.log("Device is in {0} state waiting for Managed State.".format(response['managementState'])) + if response['managementState'] == "Managed": + break + # Now we have provisioning_param so we can do wireless provisioning response = self.dnac_apply['exec']( family="wireless", @@ -1548,8 +1685,8 @@ def mandatory_parameter(self): device_type = self.config[0].get("type", "NETWORK_DEVICE") params_dict = { - "NETWORK_DEVICE": ["enable_password", "ip_address", "password", "snmp_username", "snmp_auth_passphrase", "snmp_priv_passphrase", "username"], - "COMPUTE_DEVICE": ["ip_address", "http_username", "http_password", "http_port", "snmp_username", "snmp_auth_passphrase", "snmp_priv_passphrase"], + "NETWORK_DEVICE": ["enable_password", "ip_address", "password", "snmp_username", "username"], + "COMPUTE_DEVICE": ["ip_address", "http_username", "http_password", "http_port", "snmp_username"], "MERAKI_DASHBOARD": ["http_password"], "FIREPOWER_MANAGEMENT_SYSTEM": ["ip_address", "http_username", "http_password"], "THIRD_PARTY_DEVICE": ["ip_address", "snmp_username", "snmp_auth_passphrase", "snmp_priv_passphrase"] @@ -1642,7 +1779,6 @@ def get_device_params(self, params): "httpPort": params.get("http_port"), "httpSecure": params.get("http_secure"), "httpUserName": params.get("http_username"), - "merakiOrgId": params.get("meraki_org_id"), "netconfPort": params.get("netconf_port"), "serialNumber": params.get("serial_number"), "snmpVersion": params.get("snmp_version"), @@ -1735,6 +1871,32 @@ def get_interface_from_ip(self, device_ip): log(error_message) raise Exception(error_message) + def get_device_response(self, device_ip): + """ + Args: + self (object): An instance of a class used for interacting with Cisco DNA Center. + device_ip (str): The management IP address of the device for which the response is to be retrieved. + Returns: + dict: A dictionary containing details of the device obtained from the Cisco DNA Center. + Description: + Retrieves the response of a device with the specified management IP address from Cisco DNA Center. + """ + + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"managementIpAddress": device_ip} + ) + response = response.get('response')[0] + + except Exception as e: + error_message = "Error while Getting the response of device from Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) + raise Exception(error_message) + + return response + def get_want(self, config): """ Get all the device related information from playbook that is needed to be @@ -1779,6 +1941,7 @@ def get_diff_merged(self, config): device_added = self.config[0].get("device_added", 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) if self.config[0].get('add_user_defined_field'): field_name = self.config[0].get('add_user_defined_field').get('name') @@ -1882,7 +2045,7 @@ def get_diff_merged(self, config): self.status = "success" self.result['changed'] = True self.result['response'] = execution_details - log("Device Role Updated Successfully") + self.log("Device Role Updated Successfully") msg = "Device " + str(device_to_update) + " Role updated Successfully !!" break elif execution_details.get("isError"): @@ -1956,57 +2119,59 @@ def get_diff_merged(self, config): except Exception as e: error_message = "Error while Updating Interface Details in Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) self.status = "success" self.result['changed'] = False self.msg = "Port actions are only supported on user facing/access ports as it's not allowed or No Updation required" self.log(msg) - # Update Device details and credentails - try: - self.mandatory_parameter().check_return_status() - response = self.dnac._exec( - family="devices", - function='sync_devices', - op_modifies=True, - params=self.want.get("device_params"), - ) + if credential_update: + # Update Device details and credentails + try: + self.mandatory_parameter().check_return_status() + response = self.dnac._exec( + family="devices", + function='sync_devices', + op_modifies=True, + params=self.want.get("device_params"), + ) - self.log(str(response)) + self.log(str(response)) - if response and isinstance(response, dict): - task_id = response.get('response').get('taskId') + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') - while True: - execution_details = self.get_task_details(task_id) + while True: + execution_details = self.get_task_details(task_id) - if execution_details.get("endTime"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - break - elif execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Device Updation get failed because of {0}".format(failure_reason) - else: - self.msg = "Device Updation get failed" - self.log(self.msg) - break + if execution_details.get("endTime"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device Updation get failed because of {0}".format(failure_reason) + else: + self.msg = "Device Updation get failed" + self.log(self.msg) + break - self.log("Device Updated Successfully") - self.log("Updated devices are :" + str(device_to_update)) - self.msg = "Device " + str(device_to_update) + " updated Successfully !!" - self.log(self.msg) + self.log("Device Updated Successfully") + self.log("Updated devices are :" + str(device_to_update)) + self.msg = "Device " + str(device_to_update) + " updated Successfully !!" + self.log(self.msg) - except Exception as e: - error_message = "Error while Updating device in Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) - raise Exception(error_message) + except Exception as e: + error_message = "Error while Updating device in Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) + raise Exception(error_message) - self.msg = "Devices {0} present in Cisco DNA Center and updated successfully".format(config['ip_address']) - self.log(self.msg) - self.status = "success" + self.msg = "Devices {0} present in Cisco DNA Center and updated successfully".format(config['ip_address']) + self.log(self.msg) + self.status = "success" # If we want to add device in inventory if device_added: @@ -2036,8 +2201,10 @@ def get_diff_merged(self, config): log("Device Added Successfully") log("Added devices are :" + str(devices_to_add)) msg = "Device " + str(devices_to_add) + " added Successfully !!" + self.result['msg'] = msg break msg = "Devices " + str(self.config[0].get("ip_address")) + " already present in Cisco DNA Center" + self.result['msg'] = msg break elif execution_details.get("isError"): self.status = "failed" @@ -2047,6 +2214,7 @@ def get_diff_merged(self, config): else: self.msg = "Device Addition get failed" self.log(self.msg) + self.result['msg'] = msg break except Exception as e: @@ -2054,6 +2222,38 @@ def get_diff_merged(self, config): self.log(error_message) raise Exception(error_message) + if self.config[0].get('add_user_defined_field'): + field_name = self.config[0].get('add_user_defined_field').get('name') + + if field_name is None: + self.msg = "Mandatory paramter for User Define Field - name is missing" + self.status = "failed" + return self + + # Check if the Global User defined field exist if not then create it with given field name + udf_exist = self.is_udf_exist(field_name) + + if not udf_exist: + # Create the Global UDF + self.create_user_defined_field().check_return_status() + + # Get device Id with its IP Address + device_ips = self.config[0].get("ip_address") + device_ids = self.get_device_ids(device_ips) + + if not device_ids: + self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco DNA Center" + self.status = "failed" + self.result['changed'] = False + return self + + # Now add code for adding Global UDF to device with Id + self.add_field_to_devices(device_ids).check_return_status() + + self.result['changed'] = True + self.msg = "Global User Defined Added with name {0} added to device Successfully !".format(field_name) + self.log(self.msg) + # Once Wired device get added we will assign device to site and Provisioned it if self.config[0].get('provision_wired_device'): self.provisioned_wired_device().check_return_status() @@ -2206,6 +2406,228 @@ def get_diff_deleted(self, config): self.msg = "Device Deletion get failed." self.log(self.msg) break + self.result['msg'] = self.msg + + return self + + def verify_diff_merged(self, config): + """ + Verify the merged status(Addition/Updation) of Devices in Cisco DNA Center. + Args: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + Description: + This method checks the merged status of a configuration in Cisco DNA Center by retrieving the current state + (have) and desired state (want) of the configuration, logs the states, and validates whether the devices + addition/updation/provisioning operation succeed in Cisco DNA Center. + """ + + self.get_have(config) + self.log(str(self.have)) + self.log(str(self.want)) + + devices_to_add = self.have["device_not_in_dnac"] + device_added = self.config[0].get("device_added", False) + device_updated = self.config[0].get("device_updated", False) + credential_update = self.config[0].get("credential_update", False) + device_type = self.config[0].get("type", "NETWORK_DEVICE") + + device_ips = self.config[0].get("ip_address") + + if device_added: + if not devices_to_add: + self.status = "success" + msg = "Requested Devices - {0} Added in Cisco DNA Center and Addition verified.".format(str(device_ips)) + self.log(msg) + else: + self.log("Playbook paramater doesnot match with the Cisco DNA Center means Device Addition task not executed successfully.") + + if device_updated and self.config[0].get('update_device_role'): + device_role_flag = True + + for device_ip in device_ips: + device_role_args = self.config[0].get('update_device_role') + role = device_role_args.get('role') + role_source = device_role_args.get('role_source') + response = self.get_device_response(device_ip) + + if response.get('role') != role or response.get('roleSource') != role_source: + device_role_flag = False + break + + if device_role_flag: + self.status = "success" + msg = "Device Role - {0} with Role Source {1} updated and verified successfully".format(role, role_source) + self.log(msg) + else: + self.log("Playbook paramater doesnot match with the Cisco DNA Center means Update device role task not executed successfully.") + + if device_updated and self.config[0].get('update_interface_details'): + interface_update_flag = True + + for device_ip in device_ips: + response = self.dnac._exec( + family="devices", + function='get_interface_by_ip', + params={"ip_address": device_ip} + ) + response = response.get("response")[0] + response_params = { + 'description': response.get('description'), + 'adminStatus': response.get('adminStatus'), + 'voiceVlanId': response.get('voiceVlan'), + 'vlanId': int(response.get('vlanId')) + } + + interface_playbook_params = self.config[0].get('update_interface_details') + playbook_params = { + 'description': interface_playbook_params.get('description', ''), + 'adminStatus': interface_playbook_params.get('admin_status'), + 'voiceVlanId': interface_playbook_params.get('voice_vlan_id', ''), + 'vlanId': interface_playbook_params.get('vlan_id') + } + + for key, value in playbook_params.items(): + if not value: + continue + elif response_params[key] != value: + interface_update_flag = False + break + + if not interface_update_flag: + break + + if interface_update_flag: + self.status = "success" + msg = "Interface Details are updated and verified successfully for devices {0}".format(str(device_ips)) + self.log(msg) + else: + self.log("Playbook paramater doesnot match with the Cisco DNA Center means Update Interface Details task not executed successfully.") + + if device_updated and credential_update: + credential_update_flag = True + if device_type == "NETWORK_DEVICE": + device_uuids = self.get_device_ids(device_ips) + password = "Testing@123" + payload_params = { + "deviceUuids": device_uuids, + "password": password, + "operationEnum": "0" + } + + response = self.trigger_export_api(payload_params) + csv_reader = self.decrypt_and_read_csv(response, password) + device_data = [] + for row in csv_reader: + device_data.append(row) + + csv_data_dict = { + 'snmp_retry': device_data[0]['snmp_retries'], + 'cli_transport': device_data[0]['protocol'], + 'username': device_data[0]['cli_username'], + 'password': device_data[0]['cli_password'], + 'enable_password': device_data[0]['cli_enable_password'], + 'snmp_username': device_data[0]['snmpv3_user_name'], + 'snmp_auth_protocol': device_data[0]['snmpv3_auth_type'] + } + + for key, value in csv_data_dict.items(): + if key not in config or value is None: + continue + + if key == "snmp_retry": + if int(value) != int(config[key]): + credential_update_flag = False + break + elif value != config[key]: + self.log(key) + credential_update_flag = False + break + + if credential_update_flag: + self.status = "success" + msg = "Device Credentials and details updated and verified successfully in Cisco DNA Center." + self.log(msg) + else: + self.log("Playbook paramater doesnot match with the Cisco DNA Center means Device Updation task not executed properly.") + else: + self.log("Cannot compare the paramter for device type {0} in the Playbook with Cisco DNA Center.".format(device_type)) + + if self.config[0].get('add_user_defined_field'): + field_name = self.config[0].get('add_user_defined_field').get('name') + udf_exist = self.is_udf_exist(field_name) + + if udf_exist: + self.status = "success" + msg = "Global UDF {0} created and verified successfully".format(field_name) + self.log(msg) + else: + self.log("Playbook paramater doesnot match with the Cisco DNA Center means creating Global UDF task not executed successfully.") + + if self.config[0].get('provision_wired_device'): + provision_wired_flag = True + + for device_ip in device_ips: + response = self.dnac._exec( + family="sda", + function='get_provisioned_wired_device', + op_modifies=True, + params={"device_management_ip_address": device_ip} + ) + + if response.get("status") == "failed": + description = response.get("description") + error_msg = "Cannot do Provisioning for Wired device {0} beacuse of {1}".format(device_ip, description) + self.log(error_msg) + provision_wired_flag = False + break + + if provision_wired_flag: + self.status = "success" + msg = "Wired Devices {0} gets Provisioned and verified successfully".format(str(device_ips)) + self.log(msg) + else: + self.log("Playbook paramater doesnot match with the Cisco DNA Center means Provisioning task not executed successfully.") + + return self + + def verify_diff_deleted(self, config): + """ + Verify the deletion status of Device and Global UDF in Cisco DNA Center. + Args: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + Description: + This method checks the deletion status of a configuration in Cisco DNA Center. + It validates whether the specified Devices or Global UDF deleted from Cisco DNA Center. + """ + + self.get_have(config) + self.log(str(self.have)) + self.log(str(self.want)) + devices_not_in_dnac = self.have["device_not_in_dnac"] + + if self.config[0].get('add_user_defined_field'): + field_name = self.config[0].get('add_user_defined_field').get('name') + udf_id = self.get_udf_id(field_name) + + if udf_id is None: + self.status = "success" + msg = "Global UDF - {0} deleted from Cisco DNA Center and verified successfully.".format(field_name) + self.log(msg) + return self + + if sorted(devices_not_in_dnac) == sorted(self.have["want_device"]): + self.status = "success" + msg = "Requested Devices - {0} Deleted from Cisco DNA Center and Deletion verified.".format(str(devices_not_in_dnac)) + self.log(msg) + return self + + self.log("Playbook paramater doesnot match with the Cisco DNA Center means Device Deletion task not executed successfully.") return self @@ -2223,6 +2645,7 @@ def main(): 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log': {'type': 'bool', 'default': False}, 'validate_response_schema': {'type': 'bool', 'default': True}, + 'config_verify': {'type': 'bool', "default": False}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, 'state': {'default': 'merged', 'choices': ['merged', 'deleted']} } @@ -2239,12 +2662,15 @@ def main(): dnac_device.check_return_status() dnac_device.validate_input().check_return_status() + config_verify = dnac_device.params.get("config_verify") for config in dnac_device.validated_config: dnac_device.reset_values() dnac_device.get_want(config).check_return_status() dnac_device.get_have(config).check_return_status() dnac_device.get_diff_state_apply[state](config).check_return_status() + if config_verify: + dnac_device.verify_diff_state_apply[state](config).check_return_status() module.exit_json(**dnac_device.result) From 7b9c0d0322ab950bb9b01e4e66261dcf2a74eaf0 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 12 Jan 2024 05:55:00 +0000 Subject: [PATCH 28/63] Code and playbook for config verify of device addition of PnP only --- playbooks/PnP.yml | 12 +-- plugins/modules/pnp_intent.py | 134 +++++++++++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 6 deletions(-) diff --git a/playbooks/PnP.yml b/playbooks/PnP.yml index 295bec57c7..1c3dd206ad 100644 --- a/playbooks/PnP.yml +++ b/playbooks/PnP.yml @@ -24,17 +24,18 @@ <<: *dnac_login dnac_log: True state: merged + config_verify: True config: - - site_name: Global/USA/San Francisco/BGL_18 - device_info: - - serial_number: CD2425L8M7 + - device_info: + - serial_number: QD2425L8M7 state: Unclaimed pid: c9300-24P is_sudi_required: False - - serial_number: FTC2320E0H9 + - serial_number: QTC2320E0H9 state: Unclaimed pid: c9300-24P + hostname: Test-123 - serial_number: ETC2320E0HB state: Unclaimed @@ -101,8 +102,9 @@ <<: *dnac_login dnac_log: True state: deleted + config_verify: True config: - device_info: - - serial_number: FTC2320E0HB #Will get deleted + - serial_number: QD2425L8M7 #Will get deleted - serial_number: FTC2320E0HA #Doesn't exist in the inventory - serial_number: FKC2310E0HB #Doesn't exist in the inventory diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 1305290ef8..bd26be0ac3 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -24,6 +24,10 @@ Rishita Chowdhary (@rishitachowdhary) Abinash Mishra (@abimishr) options: + config_verify: + description: Set to True to verify the Cisco DNA Center config after applying the playbook config. + type: bool + default: False state: description: The state of DNAC after module completion. type: str @@ -477,6 +481,18 @@ def get_claim_params(self): return claim_params def get_reset_params(self): + """ + Get the paramters needed for resetting the device in an errored state. + Parameters: + - self: The instance of the class containing the 'config' + attribute to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - reset_params: A dictionary needed for calling the PUT call + for update device details API. + Example: + The stored dictionary can be used to call the API update device details + """ reset_params = { "deviceResetList": [ @@ -775,6 +791,7 @@ class instance for further use. self.result['response'] = dev_add_response self.result['diff'] = self.validated_config self.result['changed'] = True + return self else: self.msg = "Device Addition Failed" self.status = "failed" @@ -806,6 +823,7 @@ class instance for further use. self.result['response'] = claim_response self.result['diff'] = self.validated_config self.result['changed'] = True + return self else: self.msg = "Device Claim Failed" self.status = "failed" @@ -823,7 +841,6 @@ class instance for further use. op_modifies=True, params=planned_count_params, ) - dev_details_response = self.dnac_apply['exec']( family="device_onboarding_pnp", function="get_device_by_id", @@ -945,6 +962,117 @@ def get_diff_deleted(self): return self + def verify_diff_merged(self, config): + """ + Verify the merged status(Creation/Updation) of site configuration in Cisco DNA Center. + Args: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + Description: + This method checks the merged status of a configuration in Cisco DNA Center by + retrieving the current state (have) and desired state (want) of the configuration, + logs the states, and validates whether the specified site exists in the DNA + Center configuration. + """ + + self.log(str(self.have)) + self.log(str(self.want)) + # Code to validate dnac config for merged state + device_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_list', + params={"serial_number": self.want.get("serial_number")} + ) + + if len(self.want.get("pnp_params")) == 1: + if not self.want["site_name"]: + if len(device_response) == 1: + self.status = "success" + msg = ( + "Requested Device with Serial No. {0} is " + "present in Cisco DNA Center and " + "addition verified.".format(self.want.get("serial_number"))) + self.log(msg) + + elif len(self.want.get("pnp_params")) > 1: + devices_added = [] + for device in self.want.get("pnp_params"): + multi_device_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_list', + params={"serial_number": device["deviceInfo"]["serialNumber"]} + ) + if (multi_device_response and (len(multi_device_response) == 1)): + msg = ( + "Requested Device with Serial No. {0} is " + "present in Cisco DNA Center and" + " addition verified.".format(device["deviceInfo"]["serialNumber"])) + self.log(msg) + else: + msg = ( + "Requested Device with Serial No. {0} is " + "not present in Cisco DNA " + "Center".format(device["deviceInfo"]["serialNumber"])) + self.status = "success" + + return self + + def verify_diff_deleted(self, config): + """ + Verify the deletion status of site configuration in Cisco DNA Center. + Args: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + Description: + This method checks the deletion status of a configuration in Cisco DNA Center. + It validates whether the specified site exists in the DNA Center configuration. + """ + + self.log(str(self.have)) + self.log(str(self.want)) + # Code to validate dnac config for merged state + device_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_list', + params={"serial_number": self.want.get("serial_number")} + ) + + if len(self.want.get("pnp_params")) == 1: + if not self.want["site_name"]: + if not len(device_response) == 1: + self.status = "success" + msg = ( + "Requested Device with Serial No. {0} is " + "not present in the Cisco DNA" + " Center.".format(self.want.get("serial_number"))) + self.log(msg) + + elif len(self.want.get("pnp_params")) > 1: + devices_added = [] + for device in self.want.get("pnp_params"): + multi_device_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_list', + params={"serial_number": device["deviceInfo"]["serialNumber"]} + ) + if not (multi_device_response and (len(multi_device_response) == 1)): + msg = ( + "Requested Device with Serial No. {0} is " + "not present in the Cisco DNA" + "Center.".format(device["deviceInfo"]["serialNumber"])) + self.log(msg) + else: + msg = ( + "Requested Device with Serial No. {0} is " + "present in Cisco DNA Center".format(device["deviceInfo"]["serialNumber"])) + self.status = "success" + + return self + def main(): """ @@ -960,6 +1088,7 @@ def main(): 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log': {'type': 'bool', 'default': False}, 'validate_response_schema': {'type': 'bool', 'default': True}, + 'config_verify': {"type": 'bool', "default": False}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, 'state': {'default': 'merged', 'choices': ['merged', 'deleted']} } @@ -975,12 +1104,15 @@ def main(): dnac_pnp.check_return_status() dnac_pnp.validate_input().check_return_status() + config_verify = dnac_pnp.params.get("config_verify") for config in dnac_pnp.validated_config: dnac_pnp.reset_values() dnac_pnp.get_want(config).check_return_status() dnac_pnp.get_have().check_return_status() dnac_pnp.get_diff_state_apply[state]().check_return_status() + if config_verify: + dnac_pnp.verify_diff_state_apply[state](config).check_return_status() module.exit_json(**dnac_pnp.result) From dae5d2673f88db33bcdfdf2115729f38231a671d Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 12 Jan 2024 11:45:13 +0530 Subject: [PATCH 29/63] Restored the automation hub signed status for further versions --- playbooks/device_details.template | 69 +++++++++++++++++++++++++++++++ playbooks/device_details.yml | 69 ------------------------------- playbooks/template_pnp_intent.yml | 4 +- 3 files changed, 71 insertions(+), 71 deletions(-) create mode 100644 playbooks/device_details.template delete mode 100644 playbooks/device_details.yml diff --git a/playbooks/device_details.template b/playbooks/device_details.template new file mode 100644 index 0000000000..38c95c627d --- /dev/null +++ b/playbooks/device_details.template @@ -0,0 +1,69 @@ +template_details: + - proj_name: 'Onboarding Configuration' + device_config: 'hostname cat9k-1\n' + language: 'velocity' + family: 'Switches and Hubs' + type: 'IOS-XE' + variant: 'XE' + temp_name: 'temp_cat9k-1' + description: 'Test Template' + export_project: + - 'Cloud DayN Templates' + export_template: + - project_name: 'Cloud DayN Templates' + template_name: 'DMVPN Spoke for Branch Router - System Default' + import_project: + do_version: false + payload: + - name: 'Onboarding Configuration2' + import_template: + do_version: false + project_name: 'Onboarding Configuration' + payload: + - name: 'Platinum-Onboarding-Template-J21' + device_types: + - product_family: 'Switches and Hubs' + productSeries: 'Cisco Catalyst 9300 Series Switches' + software_type: 'IOS' + language: 'JINJA' + - name: 'Platinum-Onboarding-Template-J22' + device_types: + - product_family: 'Switches and Hubs' + productSeries: 'Cisco Catalyst 9300 Series Switches' + software_type: 'IOS' + language: 'JINJA' + - name: 'Platinum-Onboarding-Template-J23' + device_types: + - product_family: 'Switches and Hubs' + productSeries: 'Cisco Catalyst 9300 Series Switches' + software_type: 'IOS' + language: 'JINJA' + +device_details: + - site_name: 'Global/Chennai/Trill' + image_name: 'cat9k_iosxe.17.04.01.SPA.bin' + proj_name: 'Onboarding Configuration' + temp_name: 'temp_cat9k-1' + device_version: '2' + device_number: 'AB2425L8M7' + device_name: 'Cat9k-1' + device_state: 'Unclaimed' + device_id: 'C9300-48UXM' + - site_name: 'Global/Chennai/Trill' + image_name: cat9k_iosxe.17.04.01.SPA.bin' + proj_name: 'Onboarding Configuration' + temp_name: 'temp_cat9k-2' + device_version: '2' + device_number: 'CD2425L8M7' + device_name: 'Cat9k-2' + device_state: 'Unclaimed' + device_id: 'C9300-48UXM' + - site_name: 'Global/Chennai/Trill' + image_name: 'cat9k_iosxe.17.04.01.SPA.bin' + proj_name: 'Onboarding Configuration' + temp_name: 'temp_cat9k-3' + device_version: '2' + device_number: 'EF2425L8M7' + device_name: 'Cat9k-3' + device_state: 'Unclaimed' + device_id: 'C9300-48UXM' diff --git a/playbooks/device_details.yml b/playbooks/device_details.yml deleted file mode 100644 index 4a26326e17..0000000000 --- a/playbooks/device_details.yml +++ /dev/null @@ -1,69 +0,0 @@ -template_details: - - proj_name: "Onboarding Configuration" - device_config: "hostname cat9k-1\n" - language: "velocity" - family: "Switches and Hubs" - type: "IOS-XE" - variant: "XE" - temp_name: "temp_cat9k-1" - description: "Test Template" - export_project: - - "Cloud DayN Templates" - export_template: - - project_name: "Cloud DayN Templates" - template_name: "DMVPN Spoke for Branch Router - System Default" - import_project: - do_version: false - payload: - - name: "Onboarding Configuration2" - import_template: - do_version: false - project_name: "Onboarding Configuration" - payload: - - name: "Platinum-Onboarding-Template-J21" - device_types: - - product_family: "Switches and Hubs" - productSeries: "Cisco Catalyst 9300 Series Switches" - software_type: "IOS" - language: "JINJA" - - name: "Platinum-Onboarding-Template-J22" - device_types: - - product_family: "Switches and Hubs" - productSeries: "Cisco Catalyst 9300 Series Switches" - software_type: "IOS" - language: "JINJA" - - name: "Platinum-Onboarding-Template-J23" - device_types: - - product_family: "Switches and Hubs" - productSeries: "Cisco Catalyst 9300 Series Switches" - software_type: "IOS" - language: "JINJA" - -device_details: - - site_name: "Global/Chennai/Trill" - image_name: "cat9k_iosxe.17.04.01.SPA.bin" - proj_name: "Onboarding Configuration" - temp_name: "temp_cat9k-1" - device_version: "2" - device_number: "AB2425L8M7" - device_name: "Cat9k-1" - device_state: "Unclaimed" - device_id: "C9300-48UXM" - - site_name: "Global/Chennai/Trill" - image_name: "cat9k_iosxe.17.04.01.SPA.bin" - proj_name: "Onboarding Configuration" - temp_name: "temp_cat9k-2" - device_version: "2" - device_number: "CD2425L8M7" - device_name: "Cat9k-2" - device_state: "Unclaimed" - device_id: "C9300-48UXM" - - site_name: "Global/Chennai/Trill" - image_name: "cat9k_iosxe.17.04.01.SPA.bin" - proj_name: "Onboarding Configuration" - temp_name: "temp_cat9k-3" - device_version: "2" - device_number: "EF2425L8M7" - device_name: "Cat9k-3" - device_state: "Unclaimed" - device_id: "C9300-48UXM" diff --git a/playbooks/template_pnp_intent.yml b/playbooks/template_pnp_intent.yml index c559628276..c1e2b896ac 100644 --- a/playbooks/template_pnp_intent.yml +++ b/playbooks/template_pnp_intent.yml @@ -1,7 +1,7 @@ - hosts: localhost vars_files: - - "{{ CLUSTERFILE }}" - - device_details.yml + - credentials.yml + - device_details.template gather_facts: false connection: local tasks: From 342a078d3f45762bce90f1564cd006d58f818c4d Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 12 Jan 2024 11:49:17 +0530 Subject: [PATCH 30/63] optimised code, address issue of device role not getting updated, address review comments --- plugins/modules/inventory_intent.py | 511 ++++++++++++++++------------ 1 file changed, 296 insertions(+), 215 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index b63c4ff0dc..22d330cf6c 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -920,8 +920,8 @@ def trigger_export_api(self, payload_params): Description: The function initiates the export API in Cisco DNA Center to generate a CSV file containing detailed information about devices.The response from the API includes task details and a file ID. - """ + response = self.dnac._exec( family="devices", function='export_device_list', @@ -931,6 +931,7 @@ def trigger_export_api(self, payload_params): self.log(str(response)) response = response.get("response") task_id = response.get("taskId") + while True: execution_details = self.get_task_details(task_id) @@ -945,7 +946,8 @@ def trigger_export_api(self, payload_params): else: self.msg = "Could not get the File ID so can't export device details in csv file" self.log(self.msg) - self.check_return_status() + + return response # With this File ID call the Download File by FileID API and process the response response = self.dnac._exec( @@ -975,7 +977,6 @@ def decrypt_and_read_csv(self, response, password): self.msg = "pyzipper is required for this module. Install pyzipper to use this functionality." self.log(self.msg) self.status = "failed" - return self snmp_protocol = self.config[0].get('snmp_priv_protocol', 'CISCOAES128') @@ -984,7 +985,16 @@ def decrypt_and_read_csv(self, response, password): 'CISCOAES192': 'pyzipper.WZ_AES192', 'CISCOAES256': 'pyzipper.WZ_AES' } - encryption_method = encryption_dict[snmp_protocol] + try: + encryption_method = encryption_dict.get(snmp_protocol) + except Exception as e: + self.log("Given SNMP protcol not present") + + if not encryption_method: + self.msg = "Invalid SNMP protocol specified for encryption." + self.log(self.msg) + self.status = "failed" + return self # Create a PyZipper object with the password with pyzipper.AESZipFile(zip_data, 'r', compression=pyzipper.ZIP_LZMA, encryption=encryption_method) as zip_ref: @@ -1060,11 +1070,13 @@ def export_device_details(self): } response = self.trigger_export_api(payload_params) + self.check_return_status() if payload_params["operationEnum"] == "0": temp_file_name = response.filename output_file_name = temp_file_name.split(".")[0] + ".csv" csv_reader = self.decrypt_and_read_csv(response, password) + self.check_return_status() else: encoded_resp = response.data.decode(encoding='utf-8') self.log(str(encoded_resp)) @@ -1879,7 +1891,9 @@ def get_device_response(self, device_ip): Returns: dict: A dictionary containing details of the device obtained from the Cisco DNA Center. Description: - Retrieves the response of a device with the specified management IP address from Cisco DNA Center. + This method communicates with Cisco DNA Center to retrieve the details of a device with the specified + management IP address. It executes the 'get_device_list' API call with the provided device IP address, + logs the response, and returns a dictionary containing information about the device. """ try: @@ -1897,6 +1911,144 @@ def get_device_response(self, device_ip): return response + def check_device_role(self, device_ip): + """ + Checks if the device role and role source for a device in Cisco DNA Center match the specified values in the configuration. + Args: + self (object): An instance of a class used for interacting with Cisco DNA Center. + device_ip (str): The management IP address of the device for which the device role is to be checked. + Returns: + bool: True if the device role and role source match the specified values, False otherwise. + Description: + This method retrieves the device role and role source for a device in Cisco DNA Center using the + 'get_device_response' method and compares the retrieved values with specified values in the configuration + for updating device roles. + """ + + device_role_args = self.config[0].get('update_device_role') + role = device_role_args.get('role') + role_source = device_role_args.get('role_source') + response = self.get_device_response(device_ip) + + return response.get('role') == role and response.get('roleSource') == role_source + + def check_interface_details(self, device_ip): + """ + Checks if the interface details for a device in Cisco DNA Center match the specified values in the configuration. + Args: + self (object): An instance of a class used for interacting with Cisco DNA Center. + device_ip (str): The management IP address of the device for which interface details are to be checked. + Returns: + bool: True if the interface details match the specified values, False otherwise. + Description: + This method retrieves the interface details for a device in Cisco DNA Center using the 'get_interface_by_ip' API call. + It then compares the retrieved details with the specified values in the configuration for updating interface details. + If all specified parameters match the retrieved values or are not provided in the playbook parameters, the function + returns True, indicating successful validation. + """ + + response = self.dnac._exec( + family="devices", + function='get_interface_by_ip', + params={"ip_address": device_ip} + ) + response = response.get("response")[0] + response_params = { + 'description': response.get('description'), + 'adminStatus': response.get('adminStatus'), + 'voiceVlanId': response.get('voiceVlan'), + 'vlanId': int(response.get('vlanId')) + } + + interface_playbook_params = self.config[0].get('update_interface_details') + playbook_params = { + 'description': interface_playbook_params.get('description', ''), + 'adminStatus': interface_playbook_params.get('admin_status'), + 'voiceVlanId': interface_playbook_params.get('voice_vlan_id', ''), + 'vlanId': interface_playbook_params.get('vlan_id') + } + + for key, value in playbook_params.items(): + if not value: + continue + elif response_params[key] != value: + return False + + return True + + def check_credential_update(self): + """ + Checks if the credentials for devices in the configuration match the updated values in Cisco DNA Center. + Args: + self (object): An instance of a class used for interacting with Cisco DNA Center. + Returns: + bool: True if the credentials match the updated values, False otherwise. + Description: + This method triggers the export API in Cisco DNA Center to obtain the updated credential details for + the specified devices. It then decrypts and reads the CSV file containing the updated credentials, + comparing them with the credentials specified in the configuration. + """ + + device_ips = self.config[0].get("ip_address") + device_uuids = self.get_device_ids(device_ips) + password = "Testing@123" + payload_params = {"deviceUuids": device_uuids, "password": password, "operationEnum": "0"} + response = self.trigger_export_api(payload_params) + self.check_return_status() + csv_reader = self.decrypt_and_read_csv(response, password) + self.check_return_status() + device_data = next(csv_reader, None) + + if not device_data: + return False + + csv_data_dict = { + 'snmp_retry': device_data['snmp_retries'], + 'cli_transport': device_data['protocol'], + 'username': device_data['cli_username'], + 'password': device_data['cli_password'], + 'enable_password': device_data['cli_enable_password'], + 'snmp_username': device_data['snmpv3_user_name'], + 'snmp_auth_protocol': device_data['snmpv3_auth_type'] + } + + config = self.config[0] + for key in csv_data_dict: + if key in config and csv_data_dict[key] is not None: + if key == "snmp_retry" and int(csv_data_dict[key]) != int(config[key]): + return False + elif csv_data_dict[key] != config[key]: + return False + + return True + + def get_provision_wired_device(self, device_ip): + """ + Retrieves the provisioning status of a wired device with the specified management IP address in Cisco DNA Center. + Args: + self (object): An instance of a class used for interacting with Cisco DNA Center. + device_ip (str): The management IP address of the wired device for which provisioning status is to be retrieved. + Returns: + bool: True if the device is provisioned successfully, False otherwise. + Description: + This method communicates with Cisco DNA Center to check the provisioning status of a wired device. + It executes the 'get_provisioned_wired_device' API call with the provided device IP address and + logs the response. + """ + + response = self.dnac._exec( + family="sda", + function='get_provisioned_wired_device', + op_modifies=True, + params={"device_management_ip_address": device_ip} + ) + + if response.get("status") == "failed": + self.log("Cannot do provisioning for wired device {0} because of {1}.".format(device_ip, response.get('description'))) + return False + + return True + def get_want(self, config): """ Get all the device related information from playbook that is needed to be @@ -1996,72 +2148,53 @@ def get_diff_merged(self, config): self.log(msg) return self - if self.config[0].get('update_device_role'): - for device_ip in device_to_update: - device_id = self.get_device_ids([device_ip]) - device_role_args = self.config[0].get('update_device_role') - - if 'role' not in device_role_args or 'role_source' not in device_role_args: - self.msg = "Mandatory paramter(role/sourceRole) to update Device Role are missing" - self.status = "failed" - return self - - # Check if the same role of device is present in dnac then no need to change the state + if credential_update: + # Update Device details and credentails + try: + self.mandatory_parameter().check_return_status() response = self.dnac._exec( family="devices", - function='get_device_list', - params={"managementIpAddress": device_ip} + function='sync_devices', + op_modifies=True, + params=self.want.get("device_params"), ) - response = response.get('response')[0] - if response.get('role') == device_role_args.get('role'): - self.status = "success" - self.result['changed'] = False - log_msg = "Device Role - {0} same in Cisco DNA Center as well, no updation needed".format(device_role_args.get('role')) - continue + self.log(str(response)) - device_role_params = { - 'role': device_role_args.get('role'), - 'roleSource': device_role_args.get('role_source'), - 'id': device_id[0] - } + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') - try: - response = self.dnac._exec( - family="devices", - function='update_device_role', - op_modifies=True, - params=device_role_params, - ) - self.log(str(response)) + while True: + execution_details = self.get_task_details(task_id) - if response and isinstance(response, dict): - task_id = response.get('response').get('taskId') + if execution_details.get("endTime"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device Updation get failed because of {0}".format(failure_reason) + else: + self.msg = "Device Updation get failed" + self.log(self.msg) + break - while True: - execution_details = self.get_task_details(task_id) + self.log("Device Updated Successfully") + self.log("Updated devices are :" + str(device_to_update)) + self.msg = "Device " + str(device_to_update) + " updated Successfully !!" + self.log(self.msg) - if 'successfully' in execution_details.get("progress"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - self.log("Device Role Updated Successfully") - msg = "Device " + str(device_to_update) + " Role updated Successfully !!" - break - elif execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Device Role Updation get failed because of {0}".format(failure_reason) - else: - self.msg = "Device Role Updation get failed" - self.log(self.msg) - break + except Exception as e: + error_message = "Error while Updating device in Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) + raise Exception(error_message) - except Exception as e: - error_message = "Error while Updating device role in Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) - raise Exception(error_message) + self.msg = "Devices {0} present in Cisco DNA Center and updated successfully".format(config['ip_address']) + self.log(self.msg) + self.status = "success" if self.config[0].get('update_interface_details'): # Call the Get interface details by device IP API and fetch the interface Id @@ -2123,55 +2256,75 @@ def get_diff_merged(self, config): self.status = "success" self.result['changed'] = False self.msg = "Port actions are only supported on user facing/access ports as it's not allowed or No Updation required" - self.log(msg) + self.log(self.msg) - if credential_update: - # Update Device details and credentails - try: - self.mandatory_parameter().check_return_status() + if self.config[0].get('update_device_role'): + for device_ip in device_to_update: + device_id = self.get_device_ids([device_ip]) + device_role_args = self.config[0].get('update_device_role') + + if 'role' not in device_role_args or 'role_source' not in device_role_args: + self.msg = "Mandatory paramter(role/sourceRole) to update Device Role are missing" + self.status = "failed" + return self + + # Check if the same role of device is present in dnac then no need to change the state response = self.dnac._exec( family="devices", - function='sync_devices', - op_modifies=True, - params=self.want.get("device_params"), + function='get_device_list', + params={"managementIpAddress": device_ip} ) + response = response.get('response')[0] - self.log(str(response)) + if response.get('role') == device_role_args.get('role'): + self.status = "success" + self.result['changed'] = False + log_msg = "Device Role - {0} same in Cisco DNA Center as well, no updation needed".format(device_role_args.get('role')) + self.log(log_msg) + continue - if response and isinstance(response, dict): - task_id = response.get('response').get('taskId') + device_role_params = { + 'role': device_role_args.get('role'), + 'roleSource': device_role_args.get('role_source'), + 'id': device_id[0] + } - while True: - execution_details = self.get_task_details(task_id) + try: + response = self.dnac._exec( + family="devices", + function='update_device_role', + op_modifies=True, + params=device_role_params, + ) + self.log(str(response)) - if execution_details.get("endTime"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - break - elif execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Device Updation get failed because of {0}".format(failure_reason) - else: - self.msg = "Device Updation get failed" - self.log(self.msg) - break + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') - self.log("Device Updated Successfully") - self.log("Updated devices are :" + str(device_to_update)) - self.msg = "Device " + str(device_to_update) + " updated Successfully !!" - self.log(self.msg) + while True: + execution_details = self.get_task_details(task_id) - except Exception as e: - error_message = "Error while Updating device in Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) - raise Exception(error_message) + if 'successfully' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + self.log("Device Role Updated Successfully") + msg = "Device " + str(device_to_update) + " Role updated Successfully !!" + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device Role Updation get failed because of {0}".format(failure_reason) + else: + self.msg = "Device Role Updation get failed" + self.log(self.msg) + break - self.msg = "Devices {0} present in Cisco DNA Center and updated successfully".format(config['ip_address']) - self.log(self.msg) - self.status = "success" + except Exception as e: + error_message = "Error while Updating device role in Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) + raise Exception(error_message) # If we want to add device in inventory if device_added: @@ -2420,20 +2573,27 @@ def verify_diff_merged(self, config): - self (object): An instance of a class used for interacting with Cisco DNA Center. Description: This method checks the merged status of a configuration in Cisco DNA Center by retrieving the current state - (have) and desired state (want) of the configuration, logs the states, and validates whether the devices - addition/updation/provisioning operation succeed in Cisco DNA Center. + (have) and desired state (want) of the configuration, logs the states, and validates whether the specified + site exists in the DNA Center configuration. + + The function performs the following verifications: + - Checks for devices added to Cisco DNA Center and logs the status. + - Verifies updated device roles and logs the status. + - Verifies updated interface details and logs the status. + - Verifies updated device credentials and logs the status. + - Verifies the creation of a global User Defined Field (UDF) and logs the status. + - Verifies the provisioning of wired devices and logs the status. """ self.get_have(config) - self.log(str(self.have)) - self.log(str(self.want)) + self.log("Current config in Cisco DNA Center: {0}".format(str(self.have))) + self.log("Input paramter given in Playbook config: {0}".format(str(self.want))) devices_to_add = self.have["device_not_in_dnac"] device_added = self.config[0].get("device_added", False) device_updated = self.config[0].get("device_updated", False) credential_update = self.config[0].get("credential_update", False) device_type = self.config[0].get("type", "NETWORK_DEVICE") - device_ips = self.config[0].get("ip_address") if device_added: @@ -2442,118 +2602,34 @@ def verify_diff_merged(self, config): msg = "Requested Devices - {0} Added in Cisco DNA Center and Addition verified.".format(str(device_ips)) self.log(msg) else: - self.log("Playbook paramater doesnot match with the Cisco DNA Center means Device Addition task not executed successfully.") - - if device_updated and self.config[0].get('update_device_role'): - device_role_flag = True - - for device_ip in device_ips: - device_role_args = self.config[0].get('update_device_role') - role = device_role_args.get('role') - role_source = device_role_args.get('role_source') - response = self.get_device_response(device_ip) - - if response.get('role') != role or response.get('roleSource') != role_source: - device_role_flag = False - break - - if device_role_flag: - self.status = "success" - msg = "Device Role - {0} with Role Source {1} updated and verified successfully".format(role, role_source) - self.log(msg) - else: - self.log("Playbook paramater doesnot match with the Cisco DNA Center means Update device role task not executed successfully.") + self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning device addition task not executed successfully.") if device_updated and self.config[0].get('update_interface_details'): interface_update_flag = True for device_ip in device_ips: - response = self.dnac._exec( - family="devices", - function='get_interface_by_ip', - params={"ip_address": device_ip} - ) - response = response.get("response")[0] - response_params = { - 'description': response.get('description'), - 'adminStatus': response.get('adminStatus'), - 'voiceVlanId': response.get('voiceVlan'), - 'vlanId': int(response.get('vlanId')) - } - - interface_playbook_params = self.config[0].get('update_interface_details') - playbook_params = { - 'description': interface_playbook_params.get('description', ''), - 'adminStatus': interface_playbook_params.get('admin_status'), - 'voiceVlanId': interface_playbook_params.get('voice_vlan_id', ''), - 'vlanId': interface_playbook_params.get('vlan_id') - } - - for key, value in playbook_params.items(): - if not value: - continue - elif response_params[key] != value: - interface_update_flag = False - break - - if not interface_update_flag: + if not self.check_interface_details(device_ip): + interface_update_flag = False break if interface_update_flag: self.status = "success" - msg = "Interface Details are updated and verified successfully for devices {0}".format(str(device_ips)) + msg = "Interface details updated and verified successfully for devices {0}.".format(device_ips) self.log(msg) else: - self.log("Playbook paramater doesnot match with the Cisco DNA Center means Update Interface Details task not executed successfully.") - - if device_updated and credential_update: - credential_update_flag = True - if device_type == "NETWORK_DEVICE": - device_uuids = self.get_device_ids(device_ips) - password = "Testing@123" - payload_params = { - "deviceUuids": device_uuids, - "password": password, - "operationEnum": "0" - } - - response = self.trigger_export_api(payload_params) - csv_reader = self.decrypt_and_read_csv(response, password) - device_data = [] - for row in csv_reader: - device_data.append(row) - - csv_data_dict = { - 'snmp_retry': device_data[0]['snmp_retries'], - 'cli_transport': device_data[0]['protocol'], - 'username': device_data[0]['cli_username'], - 'password': device_data[0]['cli_password'], - 'enable_password': device_data[0]['cli_enable_password'], - 'snmp_username': device_data[0]['snmpv3_user_name'], - 'snmp_auth_protocol': device_data[0]['snmpv3_auth_type'] - } + self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning update interface details task not executed successfully.") - for key, value in csv_data_dict.items(): - if key not in config or value is None: - continue + if device_updated and credential_update and device_type == "NETWORK_DEVICE": + credential_update_flag = self.check_credential_update() - if key == "snmp_retry": - if int(value) != int(config[key]): - credential_update_flag = False - break - elif value != config[key]: - self.log(key) - credential_update_flag = False - break - - if credential_update_flag: - self.status = "success" - msg = "Device Credentials and details updated and verified successfully in Cisco DNA Center." - self.log(msg) - else: - self.log("Playbook paramater doesnot match with the Cisco DNA Center means Device Updation task not executed properly.") + if credential_update_flag: + self.status = "success" + msg = "Device credentials and details updated and verified successfully in Cisco Catalyst Center." + self.log(msg) else: - self.log("Cannot compare the paramter for device type {0} in the Playbook with Cisco DNA Center.".format(device_type)) + self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning device updation task not executed properly.") + elif device_type != "NETWORK_DEVICE": + self.log("Cannot compare the parameter for device type {0} in the playbook with Cisco Catalyst Center.".format(device_type)) if self.config[0].get('add_user_defined_field'): field_name = self.config[0].get('add_user_defined_field').get('name') @@ -2566,30 +2642,35 @@ def verify_diff_merged(self, config): else: self.log("Playbook paramater doesnot match with the Cisco DNA Center means creating Global UDF task not executed successfully.") + if device_updated and self.config[0].get('update_device_role'): + device_role_flag = True + + for device_ip in device_ips: + if not self.check_device_role(device_ip): + device_role_flag = False + break + + if device_role_flag: + self.status = "success" + msg = "Device roles updated and verified successfully." + self.log(msg) + else: + self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning update device role task not executed successfully.") + if self.config[0].get('provision_wired_device'): provision_wired_flag = True for device_ip in device_ips: - response = self.dnac._exec( - family="sda", - function='get_provisioned_wired_device', - op_modifies=True, - params={"device_management_ip_address": device_ip} - ) - - if response.get("status") == "failed": - description = response.get("description") - error_msg = "Cannot do Provisioning for Wired device {0} beacuse of {1}".format(device_ip, description) - self.log(error_msg) + if not self.get_provision_wired_device(device_ip): provision_wired_flag = False break if provision_wired_flag: self.status = "success" - msg = "Wired Devices {0} gets Provisioned and verified successfully".format(str(device_ips)) + msg = "Wired devices {0} get provisioned and verified successfully.".format(device_ips) self.log(msg) else: - self.log("Playbook paramater doesnot match with the Cisco DNA Center means Provisioning task not executed successfully.") + self.log("Playbook parameter does not match with Cisco Catalyst Center, meaning provisioning task not executed successfully.") return self From 878789a657cf56d83c59eee85ed6a077449297b7 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 12 Jan 2024 11:51:52 +0530 Subject: [PATCH 31/63] Changed the Unicode quotes to the ASCII quotes --- playbooks/device_credential_intent.yml | 30 +++++++++++++------------- playbooks/network_settings_intent.yml | 6 +++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/playbooks/device_credential_intent.yml b/playbooks/device_credential_intent.yml index 2049c577d8..bd5834ffe2 100644 --- a/playbooks/device_credential_intent.yml +++ b/playbooks/device_credential_intent.yml @@ -1,6 +1,6 @@ - hosts: dnac_servers vars_files: - - credentials_245.yml + - credentials.yml gather_facts: no connection: local tasks: @@ -23,26 +23,26 @@ cli_credential: - description: CLI1 username: cli1 - password: "12345" - enable_password: "12345" + password: '12345' + enable_password: '12345' # old_description: # old_username: # id: e448ea13-4de0-406b-bc6e-f72b57ed6746 # Use this for updation or deletion snmp_v2c_read: - description: SNMPv2c Read1 # use this for deletion - read_community: "123456" + read_community: '123456' # old_description: # use this for updating the description # id: 0ee7d677-8804-43f2-8b6c-599c5f18348f # Use this for updation or deletion snmp_v2c_write: - description: SNMPv2c Write1 # use this for deletion - write_community: "123456" + write_community: '123456' # old_description: # use this for updating the description # id: a96abc1b-1fd6-41f1-8a6d-a5569c17262d # Use this for updation or deletion snmp_v3: - - auth_password: "12345678" # Atleast 8 characters + - auth_password: '12345678' # Atleast 8 characters auth_type: SHA # [SHA, MD5] (SHA is recommended) snmp_mode: AUTHPRIV # [AUTHPRIV, AUTHNOPRIV, NOAUTHNOPRIV] - privacy_password: "12345678" # Atleast 8 characters + privacy_password: '12345678' # Atleast 8 characters privacy_type: AES128 # [AE128, AE192, AE256] username: snmpV31 description: snmpV31 @@ -51,7 +51,7 @@ https_read: - description: HTTP Read1 username: HTTP_Read1 - password: "12345" + password: '12345' port: 443 # old_description: # old_username: @@ -59,7 +59,7 @@ https_write: - description: HTTP Write1 username: HTTP_Write1 - password: "12345" + password: '12345' port: 443 # old_description: # old_username: @@ -68,24 +68,24 @@ cli_credential: # description: CLI # username: cli - id: e448ea13-4de0-406b-bc6e-f72b57ed6746 + id: 2fc5f7d4-cf15-4a4f-99b3-f086e8dd6350 snmp_v2c_read: # description: SNMPv2c Read - id: 0ee7d677-8804-43f2-8b6c-599c5f18348f + id: a966a4e5-9d11-4683-8edc-a5ad8fa59ee3 snmp_v2c_write: # description: SNMPv2c Write - id: a96abc1b-1fd6-41f1-8a6d-a5569c17262d + id: 7cd072a4-2263-4087-b6ec-93b20958e286 snmp_v3: # description: snmpV3 - id: d8974823-250a-41b0-8c9b-b27b2ae01472 + id: c08a1797-84ce-4add-94a3-b419b13621e4 https_read: # description: HTTP Read # username: HTTP_Read - id: d5d7af00-5a38-4ac1-9f55-03338d00c415 + id: 1009725d-373b-4e7c-a091-300777e2bbe2 https_write: # description: HTTP Write # username: HTTP_Write - id: bec9818e-30cd-468b-bf75-292beefc2e20 + id: f1ab6e3d-01e9-4d87-8271-3ac5fde83980 site_name: - Global/Chennai/Trill - Global/Chennai/Tidel diff --git a/playbooks/network_settings_intent.yml b/playbooks/network_settings_intent.yml index e9e7fc42e3..f74d80049a 100644 --- a/playbooks/network_settings_intent.yml +++ b/playbooks/network_settings_intent.yml @@ -24,7 +24,7 @@ settings: ip_pool: - name: Global_Pool2 - gateway: "" #use this for updating + gateway: '' #use this for updating ip_address_space: IPv6 #required when we are creating cidr: 2001:db8::/64 #required when we are creating type: Generic @@ -42,7 +42,7 @@ ipv6_prefix: True ipv6_prefix_length: 64 ipv6_global_pool: 2001:db8::/64 - ipv6_subnet: "2001:db8::" + ipv6_subnet: '2001:db8::' site_name: Global/Chennai/Trill slaac_support: True # prev_name: IP_Pool_4 @@ -63,7 +63,7 @@ # shared_secret: string #ISE message_of_the_day: banner_message: hello - retain_existing_banner: "true" + retain_existing_banner: 'true' netflow_collector: ip_address: 10.0.0.4 port: 443 From 83dc378bccebd55500eaeaf147cf2b8a5893ce89 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 12 Jan 2024 12:35:26 +0530 Subject: [PATCH 32/63] print invalid protocol --- plugins/modules/inventory_intent.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 22d330cf6c..95fc1becda 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -946,7 +946,6 @@ def trigger_export_api(self, payload_params): else: self.msg = "Could not get the File ID so can't export device details in csv file" self.log(self.msg) - return response # With this File ID call the Download File by FileID API and process the response @@ -988,10 +987,10 @@ def decrypt_and_read_csv(self, response, password): try: encryption_method = encryption_dict.get(snmp_protocol) except Exception as e: - self.log("Given SNMP protcol not present") + self.log("Given SNMP protcol {0} not present".format(snmp_protocol)) if not encryption_method: - self.msg = "Invalid SNMP protocol specified for encryption." + self.msg = "Invalid SNMP protocol {0} specified for encryption.".format(snmp_protocol) self.log(self.msg) self.status = "failed" return self From 9ae50dae9de63c8c04f5ce9d6633584ee10ae112 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 12 Jan 2024 12:48:54 +0530 Subject: [PATCH 33/63] Validated the global device credentials after applying the playbook config --- plugins/modules/device_credential_intent.py | 149 +++++++++++++++++++- plugins/modules/network_settings_intent.py | 4 +- 2 files changed, 150 insertions(+), 3 deletions(-) diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index f7816cf7fa..bd71dfb5c5 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -25,6 +25,10 @@ author: Muthu Rakesh (@MUTHU-RAKESH-27) Madhan Sankaranarayanan (@madhansansel) options: + config_verify: + description: Set to True to verify the Cisco DNA Center after applying the playbook config. + type: bool + default: False state: description: The state of Cisco DNA Center after module completion. type: str @@ -1056,7 +1060,7 @@ def get_snmpV3_params(self, snmpV3Details): value = { "username": item.get("username"), "description": item.get("description"), - "snmpMode": item.get("snmp_mode"), + "snmpMode": item.get("snmpMode"), "id": item.get("id"), } if value.get("snmpMode") == "AUTHNOPRIV": @@ -2407,6 +2411,145 @@ def get_diff_deleted(self, config): return self + def verify_diff_merged(self, config): + """ + Validating the DNAC configuration with the playbook details + when state is merged (Create/Update). + + Parameters: + config (dict) - Playbook details containing Global Pool, + Reserved Pool, and Network Management configuration. + + Returns: + self + """ + + self.log(str("Entered the verify function.")) + self.get_have(config) + self.get_want(config) + self.log("DNAC retrieved details: " + str(self.have)) + self.log("Playbook details: " + str(self.want)) + if config.get("global_credential_details") is not None: + if self.want.get("want_create"): + self.msg = "Global Device Credentials config is not applied to the DNAC" + self.status = "failed" + return self + if self.want.get("want_update"): + if self.want.get("cliCredential"): + want_cli_credential = self.want.get("cliCredential") + if self.have.get("cliCredential"): + have_cli_credential = self.have.get("cliCredential") + values = ["username", "description", "id"] + for value in values: + equality = have_cli_credential.get(value) is want_cli_credential.get(value) + if not have_cli_credential or not equality: + self.msg = "CLI Credentials config is not applied ot the DNAC" + self.status = "failed" + return self + if self.want.get("snmpV2cRead"): + want_snmp_v2c_read = self.want.get("snmpV2cRead") + if self.have.get("snmpV2cRead"): + have_snmp_v2c_read = self.have.get("snmpV2cRead") + values = ["description", "id"] + for value in values: + equality = have_snmp_v2c_read.get(value) is want_snmp_v2c_read.get(value) + if not want_snmp_v2c_read or not equality: + self.msg = "snmpV2cRead Credentials config is not applied to the DNAC" + self.status = "failed" + return self + if self.want.get("snmpV2cWrite"): + want_snmp_v2c_write = self.want.get("snmpV2cWrite") + if self.have.get("snmpV2cWrite"): + have_snmp_v2c_write = self.have.get("snmpV2cWrite") + values = ["description", "id"] + for value in values: + equality = have_snmp_v2c_write.get(value) is want_snmp_v2c_write.get(value) + if not have_snmp_v2c_write or equality: + self.msg = "snmpV2cWrite Credentials config is not applied to the DNAC" + self.status = "failed" + return self + if self.want.get("httpsRead"): + want_https_read = self.want.get("httpsRead") + if self.have.get("httpsRead"): + have_https_read = self.have.get("httpsRead") + values = ["description", "username", "port", "id"] + for value in values: + equality = have_https_read.get(value) is want_https_read.get(value) + if not have_https_read or not equality: + self.msg = "httpsRead Credentials config is not applied to the DNAC" + self.status = "failed" + return self + if self.want.get("httpsWrite"): + want_https_write = self.want.get("httpsWrite") + if self.have.get("httpsWrite"): + have_https_write = self.have.get("httpsWrite") + values = ["description", "username", "port", "id"] + for value in values: + equality = have_https_write.get(value) is want_https_write.get(value) + if not have_https_write or not equality: + self.msg = "httpsWrite Credentials config is not applied to the DNAC" + self.status = "failed" + return self + if self.want.get("snmpV3"): + want_snmp_v3 = self.want.get("snmpV3") + if self.have.get("snmpV3"): + have_snmp_v3 = self.have.get("snmpV3") + values = ["username", "description", "snmpMode", "id"] + for value in values: + equality = have_snmp_v3.get(value) is have_snmp_v3.get(value) + if not have_snmp_v3 or not equality: + self.msg = "snmpV3 Credentails config is not applied to the DNAC" + self.status = "failed" + return self + + self.log("Successfully validated Global Device Credential") + self.result.get("response")[0].get("globalCredential").update({"Validation": "Success"}) + + if config.get("assign_credentials_to_site") is not None: + + self.log("Successfully validated the Assign Device Credential to site") + self.result.get("response")[0].get("assignCredential").update({"Validation": "Success"}) + + self.msg = "Successfully validated the Global Device Credential and \ + Assign Device Credential to Site." + self.status = "success" + return self + + def verify_diff_deleted(self, config): + """ + Validating the DNAC configuration with the playbook details + when state is deleted (delete). + + Parameters: + config (dict) - Playbook details containing Global Pool, + Reserved Pool, and Network Management configuration. + + Returns: + self + """ + + self.get_have(config) + self.log("DNAC retrieved details: " + str(self.have)) + self.log("Playbook details: " + str(self.want)) + if config.get("global_credential_details") is not None: + have_global_credential = self.have.get("globalCredential") + values = ["cliCredential", "snmpV2cRead", "snmpV2cWrite", + "httpsRead", "httpsWrite", "snmpV3"] + for value in values: + for item in have_global_credential.get(value): + if item is not None: + self.msg = "Delete Global Device Credentials config \ + is not applied to the config" + self.status = "failed" + return self + + self.log("Successfully validated absence of Global Device Credential.") + self.result.get("response")[0].get("globalCredential").update({"Validation": "Success"}) + + self.msg = "Successfully validated the absence of Global Device Credential." + self.status = "success" + return self + def reset_values(self): """ Reset all neccessary attributes to default values @@ -2436,6 +2579,7 @@ def main(): "dnac_version": {"type": 'str', "default": '2.2.3.3'}, "dnac_debug": {"type": 'bool', "default": False}, "dnac_log": {"type": 'bool', "default": False}, + "config_verify": {"type": 'bool', "default": False}, "config": {"type": 'list', "required": True, "elements": 'dict'}, "state": {"default": 'merged', "choices": ['merged', 'deleted']}, "validate_response_schema": {"type": 'bool', "default": True}, @@ -2445,6 +2589,7 @@ def main(): module = AnsibleModule(argument_spec=element_spec, supports_check_mode=False) dnac_credential = DnacCredential(module) state = dnac_credential.params.get("state") + config_verify = dnac_credential.params.get("config_verify") if state not in dnac_credential.supported_states: dnac_credential.status = "invalid" dnac_credential.msg = "State {0} is invalid".format(state) @@ -2458,6 +2603,8 @@ def main(): if state != "deleted": dnac_credential.get_want(config).check_return_status() dnac_credential.get_diff_state_apply[state](config).check_return_status() + if config_verify: + dnac_credential.verify_diff_state_apply[state](config).check_return_status() module.exit_json(**dnac_credential.result) diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index 3f58053d3a..3d1f319b0a 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -845,7 +845,7 @@ def get_network_params(self, site_id): } } network_settings = network_details.get("settings") - if dhcp_details.get("value") != []: + if dhcp_details and dhcp_details.get("value") != []: network_settings.update({"dhcpServer": dhcp_details.get("value")}) else: network_settings.update({"dhcpServer": [""]}) @@ -859,7 +859,7 @@ def get_network_params(self, site_id): } }) - if ntpserver_details.get("value") != []: + if ntpserver_details and ntpserver_details.get("value") != []: network_settings.update({"ntpServer": ntpserver_details.get("value")}) else: network_settings.update({"ntpServer": [""]}) From 3ad39808ae46f31647008ea4f714e34efe9ec476 Mon Sep 17 00:00:00 2001 From: Madhan Date: Fri, 12 Jan 2024 12:58:34 +0530 Subject: [PATCH 34/63] Adding device_details.template in gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 192206246c..a14cf3596b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ docs/plugins/ docs/_build/ changelogs/.plugin-cache.yaml playbooks/credentials.yml -.DS_Store \ No newline at end of file +playbooks/device_details.template +.DS_Store From 1865fb3a3d5dc9b10c90469578d27378c9010c76 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 12 Jan 2024 09:25:36 +0000 Subject: [PATCH 35/63] Chnging the docstring of config check methods in PnP --- plugins/modules/pnp_intent.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index bd26be0ac3..58ebc01363 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -973,8 +973,8 @@ def verify_diff_merged(self, config): Description: This method checks the merged status of a configuration in Cisco DNA Center by retrieving the current state (have) and desired state (want) of the configuration, - logs the states, and validates whether the specified site exists in the DNA - Center configuration. + logs the states, and validates whether the specified device(s) exists in the DNA + Center configuration's PnP Database. """ self.log(str(self.have)) @@ -1029,7 +1029,8 @@ def verify_diff_deleted(self, config): - self (object): An instance of a class used for interacting with Cisco DNA Center. Description: This method checks the deletion status of a configuration in Cisco DNA Center. - It validates whether the specified site exists in the DNA Center configuration. + It validates whether the specified device(s) exists in the DNA Center configuration's + PnP Database. """ self.log(str(self.have)) From 69b0e85540874f2512858a09e8960ecbecca15cb Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 12 Jan 2024 09:33:32 +0000 Subject: [PATCH 36/63] Chnging the docstring of config check methods in PnP --- plugins/modules/pnp_intent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 58ebc01363..1fa715986c 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -964,7 +964,7 @@ def get_diff_deleted(self): def verify_diff_merged(self, config): """ - Verify the merged status(Creation/Updation) of site configuration in Cisco DNA Center. + Verify the merged status(Creation/Updation) of PnP configuration in Cisco DNA Center. Args: - self (object): An instance of a class used for interacting with Cisco DNA Center. - config (dict): The configuration details to be verified. @@ -1021,7 +1021,7 @@ def verify_diff_merged(self, config): def verify_diff_deleted(self, config): """ - Verify the deletion status of site configuration in Cisco DNA Center. + Verify the deletion status of PnP configuration in Cisco DNA Center. Args: - self (object): An instance of a class used for interacting with Cisco DNA Center. - config (dict): The configuration details to be verified. From 1763ee961ce7f109521a30c3109aa281f86f2a13 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 12 Jan 2024 09:52:22 +0000 Subject: [PATCH 37/63] Changing the documentation in PnP --- plugins/modules/pnp_intent.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 1fa715986c..d290fd69d9 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -151,6 +151,7 @@ dnac_debug: "{{dnac_debug}}" dnac_log: True state: merged + config_verify: True config: - template_name: string image_name: string From 8050c3cd8d4287780227d916543857c2b59efed8 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 16 Jan 2024 10:58:14 +0530 Subject: [PATCH 38/63] Addressed the review comments --- plugins/modules/device_credential_intent.py | 98 ++++++--------------- 1 file changed, 28 insertions(+), 70 deletions(-) diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index bd71dfb5c5..97141f352b 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -2429,84 +2429,41 @@ def verify_diff_merged(self, config): self.get_want(config) self.log("DNAC retrieved details: " + str(self.have)) self.log("Playbook details: " + str(self.want)) + if config.get("global_credential_details") is not None: if self.want.get("want_create"): self.msg = "Global Device Credentials config is not applied to the DNAC" self.status = "failed" return self + if self.want.get("want_update"): - if self.want.get("cliCredential"): - want_cli_credential = self.want.get("cliCredential") - if self.have.get("cliCredential"): - have_cli_credential = self.have.get("cliCredential") - values = ["username", "description", "id"] - for value in values: - equality = have_cli_credential.get(value) is want_cli_credential.get(value) - if not have_cli_credential or not equality: - self.msg = "CLI Credentials config is not applied ot the DNAC" - self.status = "failed" - return self - if self.want.get("snmpV2cRead"): - want_snmp_v2c_read = self.want.get("snmpV2cRead") - if self.have.get("snmpV2cRead"): - have_snmp_v2c_read = self.have.get("snmpV2cRead") - values = ["description", "id"] - for value in values: - equality = have_snmp_v2c_read.get(value) is want_snmp_v2c_read.get(value) - if not want_snmp_v2c_read or not equality: - self.msg = "snmpV2cRead Credentials config is not applied to the DNAC" - self.status = "failed" - return self - if self.want.get("snmpV2cWrite"): - want_snmp_v2c_write = self.want.get("snmpV2cWrite") - if self.have.get("snmpV2cWrite"): - have_snmp_v2c_write = self.have.get("snmpV2cWrite") - values = ["description", "id"] - for value in values: - equality = have_snmp_v2c_write.get(value) is want_snmp_v2c_write.get(value) - if not have_snmp_v2c_write or equality: - self.msg = "snmpV2cWrite Credentials config is not applied to the DNAC" - self.status = "failed" - return self - if self.want.get("httpsRead"): - want_https_read = self.want.get("httpsRead") - if self.have.get("httpsRead"): - have_https_read = self.have.get("httpsRead") - values = ["description", "username", "port", "id"] - for value in values: - equality = have_https_read.get(value) is want_https_read.get(value) - if not have_https_read or not equality: - self.msg = "httpsRead Credentials config is not applied to the DNAC" - self.status = "failed" - return self - if self.want.get("httpsWrite"): - want_https_write = self.want.get("httpsWrite") - if self.have.get("httpsWrite"): - have_https_write = self.have.get("httpsWrite") - values = ["description", "username", "port", "id"] - for value in values: - equality = have_https_write.get(value) is want_https_write.get(value) - if not have_https_write or not equality: - self.msg = "httpsWrite Credentials config is not applied to the DNAC" - self.status = "failed" - return self - if self.want.get("snmpV3"): - want_snmp_v3 = self.want.get("snmpV3") - if self.have.get("snmpV3"): - have_snmp_v3 = self.have.get("snmpV3") - values = ["username", "description", "snmpMode", "id"] - for value in values: - equality = have_snmp_v3.get(value) is have_snmp_v3.get(value) - if not have_snmp_v3 or not equality: - self.msg = "snmpV3 Credentails config is not applied to the DNAC" - self.status = "failed" - return self + credential_types = ["cliCredential", "snmpV2cRead", "snmpV2cWrite", + "httpsRead", "httpsWrite", "snmpV3"] + value_mapping = { + "cliCredential": ["username", "description", "id"], + "snmpV2cRead": ["description", "id"], + "snmpV2cWrite": ["description", "id"], + "httpsRead": ["description", "username", "port", "id"], + "httpsWrite": ["description", "username", "port", "id"], + "snmpV3": ["username", "description", "snmpMode", "id"] + } + for credential_type in credential_types: + if self.want.get(credential_type): + want_cli_credential = self.want.get(credential_type) + if self.have.get(credential_type): + have_cli_credential = self.have.get(credential_type) + values = value_mapping.get(credential_type) + for value in values: + equality = have_cli_credential.get(value) is want_cli_credential.get(value) + if not have_cli_credential or not equality: + self.msg = "{0} config is not applied ot the DNAC".format(credential_type) + self.status = "failed" + return self self.log("Successfully validated Global Device Credential") self.result.get("response")[0].get("globalCredential").update({"Validation": "Success"}) if config.get("assign_credentials_to_site") is not None: - self.log("Successfully validated the Assign Device Credential to site") self.result.get("response")[0].get("assignCredential").update({"Validation": "Success"}) @@ -2531,12 +2488,13 @@ def verify_diff_deleted(self, config): self.get_have(config) self.log("DNAC retrieved details: " + str(self.have)) self.log("Playbook details: " + str(self.want)) + if config.get("global_credential_details") is not None: have_global_credential = self.have.get("globalCredential") - values = ["cliCredential", "snmpV2cRead", "snmpV2cWrite", + credential_types = ["cliCredential", "snmpV2cRead", "snmpV2cWrite", "httpsRead", "httpsWrite", "snmpV3"] - for value in values: - for item in have_global_credential.get(value): + for credential_type in credential_types: + for item in have_global_credential.get(credential_type): if item is not None: self.msg = "Delete Global Device Credentials config \ is not applied to the config" From a2754885bd47057a765a41bae57cb6c2d5b56c96 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 16 Jan 2024 11:14:19 +0530 Subject: [PATCH 39/63] Addressed the review comments --- plugins/modules/device_credential_intent.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index 97141f352b..68c3ce31e5 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -2449,13 +2449,13 @@ def verify_diff_merged(self, config): } for credential_type in credential_types: if self.want.get(credential_type): - want_cli_credential = self.want.get(credential_type) + want_credential = self.want.get(credential_type) if self.have.get(credential_type): - have_cli_credential = self.have.get(credential_type) + have_credential = self.have.get(credential_type) values = value_mapping.get(credential_type) for value in values: - equality = have_cli_credential.get(value) is want_cli_credential.get(value) - if not have_cli_credential or not equality: + equality = have_credential.get(value) is want_credential.get(value) + if not have_credential or not equality: self.msg = "{0} config is not applied ot the DNAC".format(credential_type) self.status = "failed" return self @@ -2492,7 +2492,7 @@ def verify_diff_deleted(self, config): if config.get("global_credential_details") is not None: have_global_credential = self.have.get("globalCredential") credential_types = ["cliCredential", "snmpV2cRead", "snmpV2cWrite", - "httpsRead", "httpsWrite", "snmpV3"] + "httpsRead", "httpsWrite", "snmpV3"] for credential_type in credential_types: for item in have_global_credential.get(credential_type): if item is not None: From c0c84e6340877d995a1b84abb735fbef8136c123 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 16 Jan 2024 12:58:22 +0530 Subject: [PATCH 40/63] Supported both the camel and snake cases --- plugins/module_utils/dnac.py | 23 +++++++++++++++++++++ plugins/modules/device_credential_intent.py | 4 ++++ plugins/modules/network_settings_intent.py | 4 ++++ plugins/modules/template_intent.py | 6 ++++-- 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 6b22e70419..0704a8d82d 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -361,6 +361,29 @@ def check_string_dictionary(self, task_details_data): pass return None + def camel_to_snake_case(self, config): + """ + Convert camel case keys to snake case keys in the config. + + Parameters: + config (list) - Playbook details provided by the user. + + Returns: + new_config (list) - Updated config after eliminating the camel cases. + """ + + if isinstance(config, dict): + new_config = {} + for key, value in config.items(): + new_key = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', key).lower() + new_value = self.camel_to_snake_case(value) + new_config[new_key] = new_value + elif isinstance(config, list): + return [self.camel_to_snake_case(item) for item in config] + else: + return config + return new_config + def log(msg, frameIncrement=0): with open('dnac.log', 'a') as of: diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index 68c3ce31e5..2614d7f997 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -840,6 +840,10 @@ def validate_input(self): } # Validate playbook params against the specification (temp_spec) + self.log("Playbook details with both cases " + str(self.config)) + self.log("Validation structure " + str(temp_spec)) + self.config = self.camel_to_snake_case(self.config) + self.log("Playbook details without camel case" + str(self.config)) valid_temp, invalid_params = validate_list_of_dicts(self.config, temp_spec) if invalid_params: self.msg = "Invalid parameters in playbook: {0}".format("\n".join(invalid_params)) diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index 3d1f319b0a..65330e4206 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -544,6 +544,10 @@ def validate_input(self): } # Validate playbook params against the specification (temp_spec) + self.log("Playbook details with both cases " + str(self.config)) + self.log("Validation structure " + str(temp_spec)) + self.config = self.camel_to_snake_case(self.config) + self.log("Playbook details without camel case" + str(self.config)) valid_temp, invalid_params = validate_list_of_dicts(self.config, temp_spec) if invalid_params: self.msg = "Invalid parameters in playbook: {0}".format("\n".join(invalid_params)) diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index ad5056336d..e077f460ee 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -1436,8 +1436,10 @@ def validate_input(self): } } # Validate template params - self.log(str(self.config)) - self.log(str(temp_spec)) + self.log("Playbook details with both cases " + str(self.config)) + self.log("Validation structure " + str(temp_spec)) + self.config = self.camel_to_snake_case(self.config) + self.log("Playbook details without camel case" + str(self.config)) valid_temp, invalid_params = validate_list_of_dicts( self.config, temp_spec ) From 7c1ee6b556be84e3b6427550855a937cac751640 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 16 Jan 2024 15:01:22 +0530 Subject: [PATCH 41/63] Added the log function in the dnac.py file for camel_to_snake_case --- playbooks/network_settings_intent.yml | 4 ++-- plugins/module_utils/dnac.py | 2 ++ plugins/modules/device_credential_intent.py | 3 --- plugins/modules/network_settings_intent.py | 11 ++++------- plugins/modules/template_intent.py | 3 --- 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/playbooks/network_settings_intent.yml b/playbooks/network_settings_intent.yml index f74d80049a..5cb7cd4bfb 100644 --- a/playbooks/network_settings_intent.yml +++ b/playbooks/network_settings_intent.yml @@ -1,6 +1,6 @@ - hosts: dnac_servers vars_files: - - credentials_245.yml + - credentials.yml gather_facts: no connection: local tasks: @@ -79,7 +79,7 @@ configure_dnac_ip: false # ip_addresses: # - 10.0.0.6 - syslogServer: + syslog_server: configure_dnac_ip: false # ip_addresses: # - 10.0.0.7 diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 0704a8d82d..b88f1a7afa 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -376,6 +376,8 @@ def camel_to_snake_case(self, config): new_config = {} for key, value in config.items(): new_key = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', key).lower() + if new_key != key: + self.log("{0} will be deprecated soon. Please use {1}.".format(key, new_key)) new_value = self.camel_to_snake_case(value) new_config[new_key] = new_value elif isinstance(config, list): diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index 2614d7f997..0cd6b79170 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -840,10 +840,7 @@ def validate_input(self): } # Validate playbook params against the specification (temp_spec) - self.log("Playbook details with both cases " + str(self.config)) - self.log("Validation structure " + str(temp_spec)) self.config = self.camel_to_snake_case(self.config) - self.log("Playbook details without camel case" + str(self.config)) valid_temp, invalid_params = validate_list_of_dicts(self.config, temp_spec) if invalid_params: self.msg = "Invalid parameters in playbook: {0}".format("\n".join(invalid_params)) diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index 65330e4206..a88b8151c3 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -256,7 +256,7 @@ elements: str type: list type: dict - syslogServer: + syslog_server: description: Network V2's syslogServer. suboptions: configure_dnac_ip: @@ -359,7 +359,7 @@ snmp_server: configure_dnac_ip: True ip_addresses: list - syslogServer: + syslog_server: configure_dnac_ip: True ip_addresses: list site_name: string @@ -499,7 +499,7 @@ def validate_input(self): "primary_ip_address": {"type": 'string'}, "secondary_ip_address": {"type": 'string'} }, - "syslogServer": { + "syslog_server": { "type": 'dict', "ip_addresses": {"type": 'list'}, "configure_dnac_ip": {"type": 'bool'} @@ -544,10 +544,7 @@ def validate_input(self): } # Validate playbook params against the specification (temp_spec) - self.log("Playbook details with both cases " + str(self.config)) - self.log("Validation structure " + str(temp_spec)) self.config = self.camel_to_snake_case(self.config) - self.log("Playbook details without camel case" + str(self.config)) valid_temp, invalid_params = validate_list_of_dicts(self.config, temp_spec) if invalid_params: self.msg = "Invalid parameters in playbook: {0}".format("\n".join(invalid_params)) @@ -1478,7 +1475,7 @@ def get_want_network(self, network_management_details): else: del want_network_settings["snmpServer"] - syslogServer = network_management_details.get("syslogServer") + syslogServer = network_management_details.get("syslog_server") if syslogServer is not None: if syslogServer.get("configure_dnac_ip") is not None: want_network_settings.get("syslogServer").update({ diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index e077f460ee..ef221201e2 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -1436,10 +1436,7 @@ def validate_input(self): } } # Validate template params - self.log("Playbook details with both cases " + str(self.config)) - self.log("Validation structure " + str(temp_spec)) self.config = self.camel_to_snake_case(self.config) - self.log("Playbook details without camel case" + str(self.config)) valid_temp, invalid_params = validate_list_of_dicts( self.config, temp_spec ) From 097ee6e3bc13e35aad975a501e02f21c2a394233 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Tue, 16 Jan 2024 19:51:00 -0500 Subject: [PATCH 42/63] Logging levels implemented in dnac.py --- plugins/module_utils/dnac.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index b88f1a7afa..5728a044c3 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -64,6 +64,7 @@ def __init__(self, module): 'parsed': self.verify_diff_parsed } self.dnac_log = dnac_params.get("dnac_log") + self.dnac_log_level = dnac_params.get("dnac_log_level").upper() #Converting to Upper Case log(str(dnac_params)) self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} @@ -145,12 +146,24 @@ def verify_diff_parsed(self): self.parsed = True return self - def log(self, message, frameIncrement=0): - """Log messages into dnac.log file""" + def log(self, message, level="info", frameIncrement=0): + """Logs/Appends messages into dnac.log file if logging is enabled and the log level is appropriate + Args: + self (obj, required): An instance of the DnacBase Class. + message (str, required): The log message to be recorded. + level (str, optional): The log level, default is "info". + The log level can be one of 'DEBUG', 'INFO', 'WARNING', 'ERROR', or 'CRITICAL'. + frameIncrement (int, optional): The number of frames to increment in the call stack, default is 0. + """ - if self.dnac_log: + level = level.upper() + if ( + self.dnac_log + and self.dnac_log_level in ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') + and logging.getLevelName(level) >= logging.getLevelName(self.dnac_log_level) + ): message = "Module: " + self.__class__.__name__ + ", " + message - log(message, (1 + frameIncrement)) + log(message, level, frameIncrement) def check_return_status(self): """API to check the return status value and exit/fail the module""" @@ -193,7 +206,8 @@ def get_dnac_params(self, params): "dnac_password": params.get("dnac_password"), "dnac_verify": params.get("dnac_verify"), "dnac_debug": params.get("dnac_debug"), - "dnac_log": params.get("dnac_log") + "dnac_log": params.get("dnac_log"), + "dnac_log_level": params.get("dnac_log_level") } return dnac_params @@ -386,15 +400,13 @@ def camel_to_snake_case(self, config): return config return new_config - -def log(msg, frameIncrement=0): +def log(msg, level='info', frameIncrement=0): with open('dnac.log', 'a') as of: callerframerecord = inspect.stack()[1 + frameIncrement] frame = callerframerecord[0] info = inspect.getframeinfo(frame) d = datetime.datetime.now().replace(microsecond=0).isoformat() - of.write("---- %s ---- %s@%s ---- %s \n" % (d, info.lineno, info.function, msg)) - + of.write("---- %s ---- %s@%s ---- %s: %s \n" % (d, info.lineno, info.function, level.upper(), msg)) def is_list_complex(x): return isinstance(x[0], dict) or isinstance(x[0], list) From 80a62941a27a784c10aacc35e8e32034176bb027 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Wed, 17 Jan 2024 05:54:35 +0000 Subject: [PATCH 43/63] Changing the documentation in PnP --- plugins/modules/pnp_intent.py | 126 +++++++++++++--------------------- 1 file changed, 46 insertions(+), 80 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index d290fd69d9..526de0ec76 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -792,11 +792,12 @@ class instance for further use. self.result['response'] = dev_add_response self.result['diff'] = self.validated_config self.result['changed'] = True - return self + else: self.msg = "Device Addition Failed" self.status = "failed" - return self + + return self else: self.log("Adding device to pnp database") @@ -824,11 +825,12 @@ class instance for further use. self.result['response'] = claim_response self.result['diff'] = self.validated_config self.result['changed'] = True - return self + else: self.msg = "Device Claim Failed" self.status = "failed" - return self + + return self prov_dev_response = self.dnac_apply['exec']( family="device_onboarding_pnp", @@ -978,46 +980,28 @@ def verify_diff_merged(self, config): Center configuration's PnP Database. """ - self.log(str(self.have)) - self.log(str(self.want)) + self.log(f"Current State (have): {self.have}") + self.log(f"Desired State (want): {self.want}") # Code to validate dnac config for merged state - device_response = self.dnac_apply['exec']( - family="device_onboarding_pnp", - function='get_device_list', - params={"serial_number": self.want.get("serial_number")} - ) - - if len(self.want.get("pnp_params")) == 1: - if not self.want["site_name"]: - if len(device_response) == 1: - self.status = "success" - msg = ( - "Requested Device with Serial No. {0} is " - "present in Cisco DNA Center and " - "addition verified.".format(self.want.get("serial_number"))) - self.log(msg) - - elif len(self.want.get("pnp_params")) > 1: - devices_added = [] - for device in self.want.get("pnp_params"): - multi_device_response = self.dnac_apply['exec']( - family="device_onboarding_pnp", - function='get_device_list', - params={"serial_number": device["deviceInfo"]["serialNumber"]} - ) - if (multi_device_response and (len(multi_device_response) == 1)): - msg = ( - "Requested Device with Serial No. {0} is " - "present in Cisco DNA Center and" - " addition verified.".format(device["deviceInfo"]["serialNumber"])) - self.log(msg) - else: - msg = ( - "Requested Device with Serial No. {0} is " - "not present in Cisco DNA " - "Center".format(device["deviceInfo"]["serialNumber"])) - self.status = "success" + for device in self.want.get("pnp_params"): + device_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_list', + params={"serial_number": device["deviceInfo"]["serialNumber"]} + ) + if (device_response and (len(device_response) == 1)): + msg = ( + "Requested Device with Serial No. {0} is " + "present in Cisco DNA Center and" + " addition verified.".format(device["deviceInfo"]["serialNumber"])) + self.log(msg) + else: + msg = ( + "Requested Device with Serial No. {0} is " + "not present in Cisco DNA " + "Center".format(device["deviceInfo"]["serialNumber"])) + self.status = "success" return self def verify_diff_deleted(self, config): @@ -1034,45 +1018,27 @@ def verify_diff_deleted(self, config): PnP Database. """ - self.log(str(self.have)) - self.log(str(self.want)) - # Code to validate dnac config for merged state - device_response = self.dnac_apply['exec']( - family="device_onboarding_pnp", - function='get_device_list', - params={"serial_number": self.want.get("serial_number")} - ) - - if len(self.want.get("pnp_params")) == 1: - if not self.want["site_name"]: - if not len(device_response) == 1: - self.status = "success" - msg = ( - "Requested Device with Serial No. {0} is " - "not present in the Cisco DNA" - " Center.".format(self.want.get("serial_number"))) - self.log(msg) - - elif len(self.want.get("pnp_params")) > 1: - devices_added = [] - for device in self.want.get("pnp_params"): - multi_device_response = self.dnac_apply['exec']( - family="device_onboarding_pnp", - function='get_device_list', - params={"serial_number": device["deviceInfo"]["serialNumber"]} - ) - if not (multi_device_response and (len(multi_device_response) == 1)): - msg = ( - "Requested Device with Serial No. {0} is " - "not present in the Cisco DNA" - "Center.".format(device["deviceInfo"]["serialNumber"])) - self.log(msg) - else: - msg = ( - "Requested Device with Serial No. {0} is " - "present in Cisco DNA Center".format(device["deviceInfo"]["serialNumber"])) - self.status = "success" + self.log(f"Current State (have): {self.have}") + self.log(f"Desired State (want): {self.want}") + # Code to validate dnac config for deleted state + for device in self.want.get("pnp_params"): + device_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_list', + params={"serial_number": device["deviceInfo"]["serialNumber"]} + ) + if not (device_response and (len(device_response) == 1)): + msg = ( + "Requested Device with Serial No. {0} is " + "not present in the Cisco DNA" + "Center.".format(device["deviceInfo"]["serialNumber"])) + self.log(msg) + else: + msg = ( + "Requested Device with Serial No. {0} is " + "present in Cisco DNA Center".format(device["deviceInfo"]["serialNumber"])) + self.status = "success" return self From a6db407b6f4160e8a4c9b258f68bb0aa045106ce Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Wed, 17 Jan 2024 06:07:53 +0000 Subject: [PATCH 44/63] Making the PnP code more efficient --- plugins/modules/pnp_intent.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 526de0ec76..be567799f2 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -980,8 +980,8 @@ def verify_diff_merged(self, config): Center configuration's PnP Database. """ - self.log(f"Current State (have): {self.have}") - self.log(f"Desired State (want): {self.want}") + self.log(f"Current State (have): {0}".format(self.have)) + self.log(f"Desired State (want): {0}".format(self.want)) # Code to validate dnac config for merged state for device in self.want.get("pnp_params"): device_response = self.dnac_apply['exec']( @@ -1018,8 +1018,8 @@ def verify_diff_deleted(self, config): PnP Database. """ - self.log(f"Current State (have): {self.have}") - self.log(f"Desired State (want): {self.want}") + self.log(f"Current State (have): {0}".format(self.have)) + self.log(f"Desired State (want): {0}".format(self.want)) # Code to validate dnac config for deleted state for device in self.want.get("pnp_params"): device_response = self.dnac_apply['exec']( From 68fde43e267cb8af171e540580c5f4f91e8a3863 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Wed, 17 Jan 2024 06:14:12 +0000 Subject: [PATCH 45/63] Making the PnP code more efficient --- plugins/modules/pnp_intent.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index be567799f2..95bedf6404 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -980,8 +980,8 @@ def verify_diff_merged(self, config): Center configuration's PnP Database. """ - self.log(f"Current State (have): {0}".format(self.have)) - self.log(f"Desired State (want): {0}".format(self.want)) + self.log("Current State (have): {0}".format(self.have)) + self.log("Desired State (want): {0}".format(self.want)) # Code to validate dnac config for merged state for device in self.want.get("pnp_params"): device_response = self.dnac_apply['exec']( @@ -1018,8 +1018,8 @@ def verify_diff_deleted(self, config): PnP Database. """ - self.log(f"Current State (have): {0}".format(self.have)) - self.log(f"Desired State (want): {0}".format(self.want)) + self.log("Current State (have): {0}".format(self.have)) + self.log("Desired State (want): {0}".format(self.want)) # Code to validate dnac config for deleted state for device in self.want.get("pnp_params"): device_response = self.dnac_apply['exec']( From 175d079298887dda45dd5471b46d33c6d61f3e5d Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Wed, 17 Jan 2024 09:41:14 +0000 Subject: [PATCH 46/63] Discovery's CIDR code along wkth documentation change, with verify method --- playbooks/discovery_intent.yml | 23 +++++- plugins/modules/discovery_intent.py | 123 ++++++++++++++++++++++++++-- 2 files changed, 137 insertions(+), 9 deletions(-) diff --git a/playbooks/discovery_intent.yml b/playbooks/discovery_intent.yml index c8044e97a3..63ebc790ae 100644 --- a/playbooks/discovery_intent.yml +++ b/playbooks/discovery_intent.yml @@ -22,6 +22,7 @@ cisco.dnac.discovery_intent: <<: *dnac_login state: merged + config_verify: True config: - devices_list: - name: SJ-BN-9300 @@ -37,8 +38,28 @@ l2interface: TenGigabitEthernet1/1/6 ip: 204.1.2.3 discovery_type: "MULTI RANGE" + discovery_name: Multi_Range_Discovery_Test protocol_order: ssh start_index: 1 records_to_return: 25 snmp_version: v2 - + + - name: Execute discovery devices using CDP/LLDP/CIDR + cisco.dnac.discovery_intent: + <<: *dnac_login + state: merged + config_verify: True + config: + - devices_list: #List length should be one + - name: SJ-BN-9300 + site: Global/USA/SAN JOSE/BLD23 + role: MAPSERVER,BORDERNODE,INTERNAL,EXTERNAL,SDATRANSIT + l2interface: TenGigabitEthernet1/1/8 + ip: 204.1.2.1 + discovery_type: "CDP" #Can be LLDP and CIDR + cdp_level: 16 #Instead use lldp for LLDP and prefix length for CIDR + discovery_name: CDP_Test_1 + protocol_order: ssh + start_index: 1 + records_to_return: 25 + snmp_version: v2 \ No newline at end of file diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index d1a22d800d..afdaad6448 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -22,6 +22,10 @@ author: Abinash Mishra (@abimishr) Phan Nguyen (phannguy) options: + config_verify: + description: Set to True to verify the Cisco DNA Center config after applying the playbook config. + type: bool + default: False state: description: The state of DNAC after module completion. type: str @@ -48,7 +52,7 @@ type: str required: true discovery_type: - description: Type of discovery (SINGLE/RANGE/MULTI RANGE/CDP/LLDP/CIDR) + description: Type of discovery (SINGLE/RANGE/MULTI RANGE/CDP/LLDP) type: str required: true cdp_level: @@ -81,7 +85,8 @@ elements: str discovery_name: description: Name of the discovery task - type: dict + type: str + required: true netconf_port: description: Port for the netconf credentials type: str @@ -176,7 +181,7 @@ dnac_log: True state: merged config: - - device_list: + - devices_list: - name: string ip: string discovery_type: string @@ -317,8 +322,9 @@ def validate_input(self): 'elements': 'str'}, 'lldp_level': {'type': 'int', 'required': False, 'default': 16}, - 'discovery_name': {'type': 'dict', 'required': False, - 'default': '{0}'.format(default_dicovery_name)}, + 'prefix_length': {'type': 'int', 'required': False, + 'default': 30}, + 'discovery_name': {'type': 'str', 'required': True}, 'netconf_port': {'type': 'str', 'required': False}, 'password_list': {'type': 'list', 'required': False, 'elements': 'str'}, @@ -391,6 +397,7 @@ def get_dnac_global_credentials_v2_info(self): params=self.validated_config[0].get('headers'), ) response = response.get('response') + self.log(response) for value in response.values(): if not value: continue @@ -436,8 +443,17 @@ def preprocessing_devices_info(self, devices_list=None): ip_address_list = [device['ip'] for device in devices_list] - if self.validated_config[0].get('discovery_type') == "SINGLE": - ip_address_list = ip_address_list[0] + if self.validated_config[0].get('discovery_type') in ["SINGLE","CDP","LLDP"]: + if len(ip_address_list) == 1: + ip_address_list = ip_address_list[0] + else: + self.module.fail_json(msg="Device list's length is longer than 1", response=[]) + elif self.validated_config[0].get('discovery_type') == "CIDR": + if len(ip_address_list) == 1 and prefix_length: + ip_address_list = ip_address_list[0] + ip_address_list = str(ip_address_list) + "/" +str(prefix_length) + else: + self.module.fail_json(msg="Device list's length is longer than 1", response=[]) else: ip_address_list = list( map( @@ -447,6 +463,7 @@ def preprocessing_devices_info(self, devices_list=None): ) ip_address_list = ','.join(ip_address_list) + self.log("Collected IP address/addresses are {0}".format(ip_address_list)) return ip_address_list def create_params(self, credential_ids=None, ip_address_list=None): @@ -510,6 +527,7 @@ def create_params(self, credential_ids=None, ip_address_list=None): new_object_params['snmpVersion'] = self.validated_config[0].get('snmp_version') new_object_params['timeout'] = self.validated_config[0].get('timeout') new_object_params['userNameList'] = self.validated_config[0].get('user_name_list') + self.log(new_object_params) return new_object_params @@ -541,6 +559,8 @@ def create_discovery(self, credential_ids=None, ip_address_list=None): op_modifies=True, ) + self.log(result) + self.result.update(dict(discovery_result=result)) return result.response.get('taskId') @@ -567,6 +587,7 @@ def get_task_status(self, task_id=None): params=params, ) response = response.response + self.log(response) if response.get('isError') or re.search( 'failed', response.get('progress'), flags=re.IGNORECASE ): @@ -605,6 +626,8 @@ def lookup_discovery_by_range_via_name(self): params=params ) + self.log(response) + return next( filter( lambda x: x['name'] == self.validated_config[0].get('discovery_name'), @@ -630,6 +653,7 @@ def get_discoveries_by_range_until_success(self): if not discovery: msg = 'Cannot find any discovery task with name {0} -- Discovery result: {1}'.format( self.validated_config[0].get("discovery_name"), discovery) + self.log(msg) self.module.fail_json(msg=msg) while True: @@ -643,6 +667,7 @@ def get_discoveries_by_range_until_success(self): if not result: msg = 'Cannot find any discovery task with name {0} -- Discovery result: {1}'.format( self.validated_config[0].get("discovery_name"), discovery) + self,log(msg) self.module.fail_json(msg=msg) self.result.update(dict(discovery_range=discovery)) @@ -677,6 +702,8 @@ def get_discovery_device_info(self, discovery_id=None, task_id=None): params=params, ) devices = response.response + + self.log(devices) if all(res.get('reachabilityStatus') == 'Success' for res in devices): result = True break @@ -704,7 +731,6 @@ def get_exist_discovery(self): returns None and updates the 'exist_discovery' entry in the result dictionary to None. """ - discovery = self.lookup_discovery_by_range_via_name() if not discovery: self.result.update(dict(exist_discovery=discovery)) @@ -732,6 +758,8 @@ def delete_exist_discovery(self, params): function="delete_discovery_by_id", params=params, ) + + self.log(response) self.result.update(dict(delete_discovery=response)) return response.response.get('taskId') @@ -795,6 +823,81 @@ def get_diff_deleted(self): return self + def verify_diff_merged(self, config): + """ + Verify the merged status(Creation/Updation) of Discovery in Cisco DNA Center. + Args: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + Description: + This method checks the merged status of a configuration in Cisco DNA Center by + retrieving the current state (have) and desired state (want) of the configuration, + logs the states, and validates whether the specified device(s) exists in the DNA + Center configuration's Discovery Database. + """ + + self.log("Current State (have): {0}".format(self.have)) + # Code to validate dnac config for merged state + discovery_task_info = self.get_discoveries_by_range_until_success() + discovery_id = discovery_task_info.get('id') + params = dict( + id=discovery_id + ) + response = self.dnac_apply['exec']( + family="discovery", + function='get_discovery_by_id', + params=params + ) + + if response: + discovery_name = response.get('response').get('name') + self.log("Requested Discovery with name {0} is completed".format(discovery_name)) + + else: + self.log("Requested Discovery with name {0} is not completed".format(discovery_name)) + self.status = "success" + + return self + + def verify_diff_deleted(self, config): + """ + Verify the deletion status of Discovery in Cisco DNA Center. + Args: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + - config (dict): The configuration details to be verified. + Return: + - self (object): An instance of a class used for interacting with Cisco DNA Center. + Description: + This method checks the deletion status of a configuration in Cisco DNA Center. + It validates whether the specified discovery(s) exists in the DNA Center configuration's + Discovery Database. + """ + + self.log("Current State (have): {0}".format(self.have)) + # Code to validate dnac config for deleted state + discovery_task_info = self.get_discoveries_by_range_until_success() + discovery_id = discovery_task_info.get('id') + params = dict( + id=discovery_id + ) + response = self.dnac_apply['exec']( + family="discovery", + function='get_discovery_by_id', + params=params + ) + + if response: + discovery_name = response.get('response').get('name') + self.log("Requested Discovery with name {0} is present".format(discovery_name)) + + else: + self.log("Requested Discovery with name {0} is not present and deleted".format(discovery_name)) + self.status = "success" + + return self + def main(): """ main entry point for module execution @@ -809,6 +912,7 @@ def main(): 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log': {'type': 'bool', 'default': False}, 'validate_response_schema': {'type': 'bool', 'default': True}, + 'config_verify': {"type": 'bool', "default": False}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, 'state': {'default': 'merged', 'choices': ['merged', 'deleted']} } @@ -817,6 +921,7 @@ def main(): supports_check_mode=False) dnac_discovery = DnacDiscovery(module) + config_verify = dnac_discovery.params.get("config_verify") state = dnac_discovery.params.get("state") if state not in dnac_discovery.supported_states: @@ -827,6 +932,8 @@ def main(): dnac_discovery.validate_input().check_return_status() for config in dnac_discovery.validated_config: dnac_discovery.get_diff_state_apply[state]().check_return_status() + if config_verify: + dnac_discovery.verify_diff_state_apply[state](config).check_return_status() module.exit_json(**dnac_discovery.result) From 38ac1fb61ebe80fb97c8a3a28421e4f021bd32b0 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Wed, 17 Jan 2024 09:45:48 +0000 Subject: [PATCH 47/63] Discovery's CIDR code along wkth documentation change, with verify method --- plugins/modules/discovery_intent.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index afdaad6448..8fc66a2824 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -443,15 +443,15 @@ def preprocessing_devices_info(self, devices_list=None): ip_address_list = [device['ip'] for device in devices_list] - if self.validated_config[0].get('discovery_type') in ["SINGLE","CDP","LLDP"]: + if self.validated_config[0].get('discovery_type') in ["SINGLE", "CDP", "LLDP"]: if len(ip_address_list) == 1: ip_address_list = ip_address_list[0] else: self.module.fail_json(msg="Device list's length is longer than 1", response=[]) elif self.validated_config[0].get('discovery_type') == "CIDR": - if len(ip_address_list) == 1 and prefix_length: + if len(ip_address_list) == 1 and self.validated_config[0].get('prefix_length'): ip_address_list = ip_address_list[0] - ip_address_list = str(ip_address_list) + "/" +str(prefix_length) + ip_address_list = str(ip_address_list) + "/" + str(self.validated_config[0].get('prefix_length')) else: self.module.fail_json(msg="Device list's length is longer than 1", response=[]) else: @@ -667,7 +667,7 @@ def get_discoveries_by_range_until_success(self): if not result: msg = 'Cannot find any discovery task with name {0} -- Discovery result: {1}'.format( self.validated_config[0].get("discovery_name"), discovery) - self,log(msg) + self.log(msg) self.module.fail_json(msg=msg) self.result.update(dict(discovery_range=discovery)) @@ -838,7 +838,7 @@ def verify_diff_merged(self, config): Center configuration's Discovery Database. """ - self.log("Current State (have): {0}".format(self.have)) + self.log(str(self.have)) # Code to validate dnac config for merged state discovery_task_info = self.get_discoveries_by_range_until_success() discovery_id = discovery_task_info.get('id') @@ -850,7 +850,7 @@ def verify_diff_merged(self, config): function='get_discovery_by_id', params=params ) - + if response: discovery_name = response.get('response').get('name') self.log("Requested Discovery with name {0} is completed".format(discovery_name)) @@ -875,7 +875,7 @@ def verify_diff_deleted(self, config): Discovery Database. """ - self.log("Current State (have): {0}".format(self.have)) + self.log(str(self.have)) # Code to validate dnac config for deleted state discovery_task_info = self.get_discoveries_by_range_until_success() discovery_id = discovery_task_info.get('id') @@ -932,8 +932,6 @@ def main(): dnac_discovery.validate_input().check_return_status() for config in dnac_discovery.validated_config: dnac_discovery.get_diff_state_apply[state]().check_return_status() - if config_verify: - dnac_discovery.verify_diff_state_apply[state](config).check_return_status() module.exit_json(**dnac_discovery.result) From 58938b7882695ed08988fc1aa4dc4dc316975685 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Wed, 17 Jan 2024 09:50:36 +0000 Subject: [PATCH 48/63] Discovery's CIDR code along wkth documentation change, with verify method --- playbooks/discovery_intent.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playbooks/discovery_intent.yml b/playbooks/discovery_intent.yml index 63ebc790ae..f285d71cdd 100644 --- a/playbooks/discovery_intent.yml +++ b/playbooks/discovery_intent.yml @@ -44,7 +44,7 @@ records_to_return: 25 snmp_version: v2 - - name: Execute discovery devices using CDP/LLDP/CIDR + - name: Execute discovery devices using CDP/LLDP/CIDR cisco.dnac.discovery_intent: <<: *dnac_login state: merged From 3d0c9af891abddab7362fbf3b0f80e4186076b44 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Thu, 18 Jan 2024 00:16:28 -0500 Subject: [PATCH 49/63] bug fix in def log under DnacBase --- plugins/module_utils/dnac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 5728a044c3..5677496079 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -163,7 +163,7 @@ def log(self, message, level="info", frameIncrement=0): and logging.getLevelName(level) >= logging.getLevelName(self.dnac_log_level) ): message = "Module: " + self.__class__.__name__ + ", " + message - log(message, level, frameIncrement) + log(message, level,(1 + frameIncrement)) def check_return_status(self): """API to check the return status value and exit/fail the module""" From b149ac7baf12a61848ec967f99e73605376fed27 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Thu, 18 Jan 2024 06:18:20 +0000 Subject: [PATCH 50/63] Making few changes in documentation of Discovery --- plugins/modules/discovery_intent.py | 30 +++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 8fc66a2824..5ed392921d 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -64,14 +64,14 @@ type: int default: 16 start_index: - description: Start index for the header in fetching global v2 credentials + description: Start index for the header in fetching SNMP v2 credentials type: int enable_password_list: description: List of enable passwords for the CLI crfedentials type: list elements: str records_to_return: - description: Number of records to returnfor the header in fetching global v2 credentials + description: Number of records to return for the header in fetching global v2 credentials type: int http_read_credential: description: HTTP read credentials for hosting a device @@ -188,7 +188,7 @@ cdp_level: string lldp_level: string start_index: integer - enable_pasword_list: list + enable_password_list: list records_to_return: integer http_read_credential: string http_write_credential: string @@ -201,7 +201,7 @@ snmp_auth_passphrase: string snmp_auth_protocol: string snmp_mode: string - snmp_priv_passphrse: string + snmp_priv_passphrase: string snmp_priv_protocol: string snmp_ro_community: string snmp_ro_community_desc: string @@ -211,6 +211,24 @@ snmp_version: string timeout: integer username_list: list +- name: Delete disovery by name + 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 + state: deleted + config: + - devices_list: + - name: string + ip: string + start_index: integer + records_to_return: integer + discovery_name: string """ RETURN = r""" @@ -305,7 +323,6 @@ def validate_input(self): self.status = "success" return self - default_dicovery_name = 'discovery_' + str(time.time()) discovery_spec = { 'cdp_level': {'type': 'int', 'required': False, 'default': 16}, @@ -314,7 +331,8 @@ def validate_input(self): 'elements': 'str'}, 'devices_list': {'type': 'list', 'required': True, 'elements': 'dict'}, - 'start_index': {'type': 'int', 'required': False}, + 'start_index': {'type': 'int', 'required': False, + 'default': 25}, 'records_to_return': {'type': 'int', 'required': False}, 'http_read_credential': {'type': 'dict', 'required': False}, 'http_write_credential': {'type': 'dict', 'required': False}, From f1a04d0a66d0387f726572e6858160c88710cab1 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 18 Jan 2024 20:36:07 +0530 Subject: [PATCH 51/63] Address Jira ticket issue of SWIM, Inventory, Site module and update documentation with examples --- plugins/modules/inventory_intent.py | 119 ++++++++++++++++++++++------ plugins/modules/site_intent.py | 6 +- plugins/modules/swim_intent.py | 107 +++++++++++++++++-------- 3 files changed, 172 insertions(+), 60 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 95fc1becda..8df7864ef0 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -512,6 +512,7 @@ admin_status: str vlan_id: int voice_vlan_id: int + deployment_mode: str - name: Export Device Details in a CSV file Interface details with IP Address cisco.dnac.inventory_intent: @@ -1109,6 +1110,41 @@ def export_device_details(self): return self + def check_ap_devices(self, device_ips): + """ + Args: + self (object): An instance of a class used for interacting with Cisco DNA Center. + device_ip (str): The management IP address of the device for which the response is to be retrieved. + Returns: + list: A list containing Access Point device IP's obtained from the Cisco DNA Center. + Description: + This method communicates with Cisco DNA Center to retrieve the details of a device with the specified + management IP address and check if device family matched to Unified AP. It executes the 'get_device_list' + API call with the provided device IP address, logs the response, and returns list containing ap device ips. + """ + + ap_device_list = [] + for device_ip in device_ips: + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"managementIpAddress": device_ip} + ) + response = response.get('response') + if not response: + continue + response = response[0] + if response['family'] == "Unified AP": + ap_device_list.append(device_ip) + + except Exception as e: + error_message = "Error while Getting the response of device from Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) + raise Exception(error_message) + + return ap_device_list + def resync_devices(self): """ Resync devices in Cisco DNA Center. @@ -1127,14 +1163,21 @@ def resync_devices(self): device_ips = self.config[0].get("ip_address", []) if not device_ips: - msg = "Cannot perform the Resync operation as device's are not present in Cisco DNA Center" - self.status = "failed" - self.msg = msg - self.log(msg) + self.msg = "Cannot perform the Resync operation as device's {0} are not present in Cisco DNA Center".format(str(device_ips)) + self.status = "success" + self.result['changed'] = False + self.log(self.msg) return self - device_ids = self.get_device_ids(device_ips) + ap_devices = self.check_ap_devices(device_ips) + self.log("AP Devices from the playbook input are : {0}".format(str(ap_devices))) + if ap_devices: + for ap_ip in ap_devices: + device_ips.remove(ap_ip) + self.log("Following devices {0} are AP so can't perform resync operation.".format(str(ap_devices))) + + device_ids = self.get_device_ids(device_ips) try: force_sync = self.config[0].get("force_sync", False) resync_param_dict = { @@ -1193,32 +1236,44 @@ def reboot_access_points(self): """ device_ips = self.config[0].get("ip_address", []) + if device_ips: + ap_devices = self.check_ap_devices(device_ips) + self.log("AP Devices from the playbook input are : {0}".format(str(ap_devices))) + for device_ip in device_ips: + if device_ip not in ap_devices: + device_ips.remove(device_ip) if not device_ips: self.msg = "No AP Devices IP given in the playbook so can't perform reboot operation" self.status = "success" self.result['changed'] = False + self.result['response'] = self.msg self.log(self.msg) return self - ap_mac_address_list = [] # Get and store the apEthernetMacAddress of given devices + ap_mac_address_list = [] for device_ip in device_ips: response = self.dnac._exec( family="devices", function='get_device_list', params={"managementIpAddress": device_ip} ) - response = response.get('response')[0] + response = response.get('response') + if not response: + continue + + response = response[0] ap_mac_address = response.get('apEthernetMacAddress') if ap_mac_address is not None: ap_mac_address_list.append(ap_mac_address) if not ap_mac_address_list: - self.status = "failed" + self.status = "success" self.result['changed'] = False self.msg = "Cannot find the AP devices for rebooting" + self.result['response'] = self.msg self.log(self.msg) return self @@ -1255,7 +1310,7 @@ def reboot_access_points(self): break self.log("AP Devices Rebooted Successfully and Rebooted devices are :" + str(device_ips)) - msg = "Device " + str(device_ips) + " Rebooted Successfully !!" + self.msg = "Device " + str(device_ips) + " Rebooted Successfully !!" return self @@ -1405,6 +1460,7 @@ def provisioned_wired_device(self): self.status = "failed" self.msg = "Site/Devices are required for Provisioning of Wired Devices." self.log(self.msg) + self.result['response'] = self.msg return self provision_wired_params = { @@ -2380,6 +2436,7 @@ def get_diff_merged(self, config): if field_name is None: self.msg = "Mandatory paramter for User Define Field - name is missing" self.status = "failed" + self.result['response'] = self.msg return self # Check if the Global User defined field exist if not then create it with given field name @@ -2397,6 +2454,7 @@ def get_diff_merged(self, config): self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco DNA Center" self.status = "failed" self.result['changed'] = False + self.result['response'] = self.msg return self # Now add code for adding Global UDF to device with Id @@ -2515,16 +2573,19 @@ def get_diff_deleted(self, config): function='delete_provisioned_wired_device', params=provision_params, ) - if response.get("status") == "success": - msg = "Wired device {0} unprovisioned successfully.".format(device_ip) - self.log(msg) - self.result['changed'] = True - self.status = "success" - else: - msg = response.get("description") - self.log(msg) - self.status = "failed" - + executionid = response.get("executionId") + while True: + execution_details = self.get_execution_details(executionid) + if execution_details.get("status") == "SUCCESS": + self.result['changed'] = True + self.msg = execution_details.get("bapiName") + self.log(self.msg) + self.result['response'] = self.msg + break + elif execution_details.get("bapiError"): + self.msg = execution_details.get("bapiError") + self.log(self.msg) + break except Exception as e: device_id = self.get_device_ids([device_ip]) delete_params = { @@ -2689,7 +2750,8 @@ def verify_diff_deleted(self, config): self.get_have(config) self.log(str(self.have)) self.log(str(self.want)) - devices_not_in_dnac = self.have["device_not_in_dnac"] + input_devices = self.have["want_device"] + device_in_dnac = self.device_exists_in_dnac() if self.config[0].get('add_user_defined_field'): field_name = self.config[0].get('add_user_defined_field').get('name') @@ -2701,13 +2763,18 @@ def verify_diff_deleted(self, config): self.log(msg) return self - if sorted(devices_not_in_dnac) == sorted(self.have["want_device"]): - self.status = "success" - msg = "Requested Devices - {0} Deleted from Cisco DNA Center and Deletion verified.".format(str(devices_not_in_dnac)) - self.log(msg) - return self + device_delete_flag = True + for device_ip in input_devices: + if device_ip in device_in_dnac: + device_delete_flag = False + break - self.log("Playbook paramater doesnot match with the Cisco DNA Center means Device Deletion task not executed successfully.") + if device_delete_flag: + self.status = "success" + self.msg = "Requested Devices - {0} Deleted from Cisco DNA Center and Deletion verified.".format(str(input_devices)) + self.log(self.msg) + else: + self.log("Playbook paramater doesnot match with the Cisco DNA Center means Device Deletion task not executed successfully.") return self diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index ea8d7c5849..a76dc45b55 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -56,7 +56,7 @@ description: Name of the area (eg Area1). type: str parentName: - description: Parent name of the area to be created. + description: Complete Parent name of the Area to be created/deleted(eg Global/). type: str building: description: Building Details. @@ -75,7 +75,7 @@ description: Name of the building (eg building1). type: str parent_name: - description: Parent name of building to be created. + description: Complete Parent name of the Building to be created/deleted(eg Global/USA/San Francisco). type: str floor: description: Site Create's floor. @@ -1010,7 +1010,7 @@ def main(): 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log': {'type': 'bool', 'default': False}, 'validate_response_schema': {'type': 'bool', 'default': True}, - "config_verify": {"type": 'bool', "default": False}, + 'config_verify': {'type': 'bool', "default": False}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, 'state': {'default': 'merged', 'choices': ['merged', 'deleted']} } diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index b21bf2a1e4..33f37b0d50 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -234,10 +234,10 @@ config: - import_image_details: type: string - urlDetails: + url_details: payload: - source_url: string - is_third_party: bool + third_party: bool image_family: string vendor: string application_type: string @@ -260,6 +260,33 @@ device_serial_number: string image_name: string +- name: Import an image from local, tag it as golden. + cisco.dnac.swim_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 + config: + - import_image_details: + type: string + local_image_details: + file_path: string + is_third_party: bool + third_party_vendor: string + third_party_image_family: string + third_party_application_type: string + tagging_details: + image_name: string + device_role: string + device_family_name: string + device_type: string + site_name: string + tagging: bool + - name: Tag the given image as golden and load it on device cisco.dnac.swim_intent: dnac_host: "{{dnac_host}}" @@ -472,6 +499,7 @@ def get_image_id(self, name): log("Image Id: " + str(image_id)) else: error_message = "Image {0} not found".format(name) + self.log(error_message) self.module.fail_json(msg="Image not found", response=image_response) return image_id @@ -675,9 +703,9 @@ def get_have(self): device_params = dict( hostname=distribution_details.get("device_hostname"), - serial_number=distribution_details.get("device_serial_number"), - management_ip_address=distribution_details.get("device_ip_address"), - mac_address=distribution_details.get("device_mac_address"), + serialNumber=distribution_details.get("device_serial_number"), + managementIpAddress=distribution_details.get("device_ip_address"), + macAddress=distribution_details.get("device_mac_address"), ) device_id = self.get_device_id(device_params) if device_id is not None: @@ -709,9 +737,9 @@ def get_have(self): device_params = dict( hostname=activation_details.get("device_hostname"), - serial_number=activation_details.get("device_serial_number"), - management_ip_address=activation_details.get("device_ip_address"), - mac_address=activation_details.get("device_mac_address"), + serialNumber=activation_details.get("device_serial_number"), + managementIpAddress=activation_details.get("device_ip_address"), + macAddress=activation_details.get("device_mac_address"), ) device_id = self.get_device_id(device_params) if device_id is not None: @@ -776,12 +804,19 @@ def get_diff_import(self): if import_type == "url": image_name = self.want.get("url_import_details").get("payload")[0].get("source_url") else: - image_name = self.want.get("local_import_details").get("filePath") + image_name = self.want.get("local_import_details").get("file_path") # Code to check if the image already exists in DNAC name = image_name.split('/')[-1] image_exist = self.is_image_exist(name) + import_key_mapping = { + 'source_url': 'sourceURL', + 'image_family': 'imageFamily', + 'application_type': 'applicationType', + 'third_party': 'thirdParty', + } + if image_exist: image_id = self.get_image_id(name) self.have["imported_image_id"] = image_id @@ -793,19 +828,29 @@ def get_diff_import(self): return self if self.want.get("import_type") == "url": + import_payload_dict = {} + temp_payload = self.want.get("url_import_details").get("payload")[0] + keys_to_change = list(import_key_mapping.keys()) + + for key, val in temp_payload.items(): + if key in keys_to_change: + api_key_name = import_key_mapping[key] + import_payload_dict[api_key_name] = val + + import_image_payload = [import_payload_dict] import_params = dict( - payload=self.want.get("url_import_details").get("payload"), - schedule_at=self.want.get("url_import_details").get("schedule_at"), - schedule_desc=self.want.get("url_import_details").get("schedule_desc"), - schedule_origin=self.want.get("url_import_details").get("schedule_origin"), + payload=import_image_payload, + scheduleAt=self.want.get("url_import_details").get("schedule_at"), + scheduleDesc=self.want.get("url_import_details").get("schedule_desc"), + scheduleOrigin=self.want.get("url_import_details").get("schedule_origin"), ) import_function = 'import_software_image_via_url' else: import_params = dict( - is_third_party=self.want.get("local_import_details").get("is_third_party"), - third_party_vendor=self.want.get("local_import_details").get("third_party_vendor"), - third_party_image_family=self.want.get("local_import_details").get("third_party_image_family"), - third_party_application_type=self.want.get("local_import_details").get("third_party_application_type"), + isThirdParty=self.want.get("local_import_details").get("is_third_party"), + thirdPartyVendor=self.want.get("local_import_details").get("third_party_vendor"), + thirdPartyImageFamily=self.want.get("local_import_details").get("third_party_image_family"), + thirdPartyApplicationType=self.want.get("local_import_details").get("third_party_application_type"), file_path=self.want.get("local_import_details").get("file_path"), ) import_function = 'import_local_software_image' @@ -858,7 +903,7 @@ def get_diff_import(self): except Exception as e: self.log("Import Image details are not given in the playbook") - self.status = "success" + self.status = "failed" self.result['changed'] = False return self @@ -900,10 +945,10 @@ def get_diff_tagging(self): else: image_params = dict( - image_id=self.have.get("tagging_image_id"), - site_id=self.have.get("site_id"), - device_family_identifier=self.have.get("device_family_identifier"), - device_role=tagging_details.get("deviceRole") + imageId=self.have.get("tagging_image_id"), + siteId=self.have.get("site_id"), + deviceFamilyIdentifier=self.have.get("device_family_identifier"), + deviceRole=tagging_details.get("deviceRole") ) log("Image params for un-tagging image as golden:" + str(image_params)) @@ -978,13 +1023,13 @@ def get_diff_distribution(self): break if task_details.get("isError"): - error_msg = "Image with Id {0} Distribution Failed".format(image_id) self.status = "failed" + self.msg = "Image with Id {0} Distribution Failed".format(image_id) + self.log(self.msg) self.result['response'] = task_details - self.msg = error_msg - return self + break - self.result['response'] = task_details if task_details else response + self.result['response'] = task_details if task_details else response return self @@ -1033,18 +1078,18 @@ def get_diff_distribution(self): if device_distribution_count == 0: self.status = "failed" - msg = "Image with Id {0} Distribution Failed for all devices".format(image_id) + self.msg = "Image with Id {0} Distribution Failed for all devices".format(image_id) elif device_distribution_count == len(device_uuid_list): self.result['changed'] = True self.status = "success" - msg = "Image with Id {0} Distributed Successfully for all devices".format(image_id) + self.msg = "Image with Id {0} Distributed Successfully for all devices".format(image_id) else: self.result['changed'] = True self.status = "success" - msg = "Image with Id {0} Distributed and partially Successfull".format(image_id) + self.msg = "Image with Id {0} Distributed and partially Successfull".format(image_id) - self.result['msg'] = msg - self.log(msg) + self.result['msg'] = self.msg + self.log(self.msg) return self From ce8ee03dc4f7475eb85ea3c5338963fdb63e79e4 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 19 Jan 2024 09:56:35 +0530 Subject: [PATCH 52/63] address PR review comments --- plugins/modules/inventory_intent.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 8df7864ef0..6495e6be46 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -1110,7 +1110,7 @@ def export_device_details(self): return self - def check_ap_devices(self, device_ips): + def get_ap_devices(self, device_ips): """ Args: self (object): An instance of a class used for interacting with Cisco DNA Center. @@ -1131,15 +1131,12 @@ def check_ap_devices(self, device_ips): function='get_device_list', params={"managementIpAddress": device_ip} ) - response = response.get('response') - if not response: - continue - response = response[0] - if response['family'] == "Unified AP": - ap_device_list.append(device_ip) + response = response.get('response', []) + if response and response[0].get('family', '') == "Unified AP": + ap_device_list.append(device_ip) except Exception as e: - error_message = "Error while Getting the response of device from Cisco DNA Center - {0}".format(str(e)) + error_message = "Error while getting the response of device from Cisco DNA Center - {0}".format(str(e)) self.log(error_message) raise Exception(error_message) @@ -1163,19 +1160,19 @@ def resync_devices(self): device_ips = self.config[0].get("ip_address", []) if not device_ips: - self.msg = "Cannot perform the Resync operation as device's {0} are not present in Cisco DNA Center".format(str(device_ips)) + self.msg = "Cannot perform the Resync operation as device's {0} are not present inCisco Catalyst Center".format(str(device_ips)) self.status = "success" self.result['changed'] = False self.log(self.msg) return self - ap_devices = self.check_ap_devices(device_ips) - self.log("AP Devices from the playbook input are : {0}".format(str(ap_devices))) + ap_devices = self.get_ap_devices(device_ips) + self.log("AP Devices from the playbook input are: {0}".format(str(ap_devices))) if ap_devices: for ap_ip in ap_devices: device_ips.remove(ap_ip) - self.log("Following devices {0} are AP so can't perform resync operation.".format(str(ap_devices))) + self.log("Following devices {0} are AP, so can't perform resync operation.".format(str(ap_devices))) device_ids = self.get_device_ids(device_ips) try: @@ -1237,7 +1234,7 @@ def reboot_access_points(self): device_ips = self.config[0].get("ip_address", []) if device_ips: - ap_devices = self.check_ap_devices(device_ips) + ap_devices = self.get_ap_devices(device_ips) self.log("AP Devices from the playbook input are : {0}".format(str(ap_devices))) for device_ip in device_ips: if device_ip not in ap_devices: From 100853c823f89a063adf6cb4353da2b9245ea81a Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Fri, 19 Jan 2024 00:20:01 -0500 Subject: [PATCH 53/63] sanity test fixes --- plugins/module_utils/dnac.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 5677496079..fc77f18ae5 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -64,7 +64,7 @@ def __init__(self, module): 'parsed': self.verify_diff_parsed } self.dnac_log = dnac_params.get("dnac_log") - self.dnac_log_level = dnac_params.get("dnac_log_level").upper() #Converting to Upper Case + self.dnac_log_level = dnac_params.get("dnac_log_level").upper() log(str(dnac_params)) self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} @@ -151,7 +151,7 @@ def log(self, message, level="info", frameIncrement=0): Args: self (obj, required): An instance of the DnacBase Class. message (str, required): The log message to be recorded. - level (str, optional): The log level, default is "info". + level (str, optional): The log level, default is "info". The log level can be one of 'DEBUG', 'INFO', 'WARNING', 'ERROR', or 'CRITICAL'. frameIncrement (int, optional): The number of frames to increment in the call stack, default is 0. """ @@ -163,7 +163,7 @@ def log(self, message, level="info", frameIncrement=0): and logging.getLevelName(level) >= logging.getLevelName(self.dnac_log_level) ): message = "Module: " + self.__class__.__name__ + ", " + message - log(message, level,(1 + frameIncrement)) + log(message, level, (1 + frameIncrement)) def check_return_status(self): """API to check the return status value and exit/fail the module""" @@ -400,6 +400,7 @@ def camel_to_snake_case(self, config): return config return new_config + def log(msg, level='info', frameIncrement=0): with open('dnac.log', 'a') as of: callerframerecord = inspect.stack()[1 + frameIncrement] @@ -408,6 +409,7 @@ def log(msg, level='info', frameIncrement=0): d = datetime.datetime.now().replace(microsecond=0).isoformat() of.write("---- %s ---- %s@%s ---- %s: %s \n" % (d, info.lineno, info.function, level.upper(), msg)) + def is_list_complex(x): return isinstance(x[0], dict) or isinstance(x[0], list) From ee980d7f2247016bb42b250da60c3cef8628f96f Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 19 Jan 2024 12:34:13 +0530 Subject: [PATCH 54/63] Validated the DNAC after applying the configuration template config --- playbooks/template_pnp_intent.yml | 1 + plugins/modules/template_intent.py | 83 ++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/playbooks/template_pnp_intent.yml b/playbooks/template_pnp_intent.yml index c1e2b896ac..09ea6a7226 100644 --- a/playbooks/template_pnp_intent.yml +++ b/playbooks/template_pnp_intent.yml @@ -18,6 +18,7 @@ dnac_debug: "{{ dnac_debug }}" dnac_log: true state: "merged" + config_verify: true #ignore_errors: true #Enable this to continue execution even the task fails config: - configuration_templates: diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index ef221201e2..055ec89f0d 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -30,6 +30,10 @@ Akash Bhaskaran (@akabhask) Muthu Rakesh (@MUTHU-RAKESH-27) options: + config_verify: + description: Set to True to verify the Cisco DNA Center after applying the playbook config. + type: bool + default: False state: description: The state of DNAC after module completion. type: str @@ -2635,6 +2639,81 @@ def get_diff_deleted(self, config): self.status = "success" return self + def verify_diff_merged(self, config): + """ + Validating the DNAC configuration with the playbook details + when state is merged (Create/Update). + + Parameters: + config (dict) - Playbook details containing Global Pool, + Reserved Pool, and Network Management configuration. + + Returns: + self + """ + + if config.get("configuration_templates") is not None: + template_available = self.get_have_project(config) + self.log(str(template_available)) + if not template_available: + self.msg = "Configuration Template config is not applied to the DNAC." + self.status = "failed" + return self + + self.get_have_template(config, template_available) + self.log("DNAC retrieved details: " + str(self.have_template.get("template"))) + self.log("Playbook details: " + str(self.want.get("template_params"))) + template_params = ["language", "name", "projectName", "softwareType", + "softwareVariant", "templateContent"] + for item in template_params: + if self.have_template.get("template").get(item) != self.want.get("template_params").get(item): + self.msg = " Configuration Template config is not applied to the DNAC." + self.status = "failed" + return self + self.result.get("response").update({"Validation": "Success"}) + + self.msg = "Successfully validated the Configuration Templates." + self.status = "success" + return self + + def verify_diff_deleted(self, config): + """ + Validating the DNAC configuration with the playbook details + when state is deleted (delete). + + Parameters: + config (dict) - Playbook details containing Global Pool, + Reserved Pool, and Network Management configuration. + + Returns: + self + """ + + if config.get("configuration_templates") is not None: + self.log("DNAC retrieved details: " + str(self.have)) + self.log("Playbook details: " + str(self.want)) + template_list = self.dnac_apply['exec']( + family="configuration_templates", + function="gets_the_templates_available", + params={"projectNames": config.get("projectName")}, + ) + if template_list and isinstance(template_list, list): + templateName = config.get("configuration_templates").get("template_name") + template_info = get_dict_result(template_list, + "name", + templateName) + if template_info: + self.msg = "Configuration Template config is not applied to the DNAC." + self.status = "failed" + return self + + self.log("Successfully validated absence of Template in the DNAC.") + self.result.get("response").update({"Validation": "Success"}) + + self.msg = "Successfully validated the absence of Template in the DNAC." + self.status = "success" + return self + def reset_values(self): """ Reset all neccessary attributes to default values. @@ -2663,6 +2742,7 @@ def main(): 'dnac_debug': {'type': 'bool', 'default': False}, 'dnac_log': {'type': 'bool', 'default': False}, 'validate_response_schema': {'type': 'bool', 'default': True}, + "config_verify": {"type": 'bool', "default": False}, 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, 'state': {'default': 'merged', 'choices': ['merged', 'deleted']} } @@ -2671,6 +2751,7 @@ def main(): dnac_template = DnacTemplate(module) dnac_template.validate_input().check_return_status() state = dnac_template.params.get("state") + config_verify = dnac_template.params.get("config_verify") if state not in dnac_template.supported_states: dnac_template.status = "invalid" dnac_template.msg = "State {0} is invalid".format(state) @@ -2681,6 +2762,8 @@ def main(): dnac_template.get_have(config).check_return_status() dnac_template.get_want(config).check_return_status() dnac_template.get_diff_state_apply[state](config).check_return_status() + if config_verify: + dnac_template.verify_diff_state_apply[state](config).check_return_status() module.exit_json(**dnac_template.result) From c7553d075d27aa6bb28d131cb9783e76383f3335 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 19 Jan 2024 14:03:42 +0530 Subject: [PATCH 55/63] Addressed the PR comments --- plugins/modules/template_intent.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index 055ec89f0d..33448313a7 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -2653,14 +2653,14 @@ def verify_diff_merged(self, config): """ if config.get("configuration_templates") is not None: - template_available = self.get_have_project(config) - self.log(str(template_available)) - if not template_available: + is_template_available = self.get_have_project(config) + self.log(str(is_template_available)) + if not is_template_available: self.msg = "Configuration Template config is not applied to the DNAC." self.status = "failed" return self - self.get_have_template(config, template_available) + self.get_have_template(config, is_template_available) self.log("DNAC retrieved details: " + str(self.have_template.get("template"))) self.log("Playbook details: " + str(self.want.get("template_params"))) template_params = ["language", "name", "projectName", "softwareType", @@ -2670,7 +2670,7 @@ def verify_diff_merged(self, config): self.msg = " Configuration Template config is not applied to the DNAC." self.status = "failed" return self - self.result.get("response").update({"Validation": "Success"}) + self.result.get("response").update({"Validation": "Success"}) self.msg = "Successfully validated the Configuration Templates." self.status = "success" @@ -2702,10 +2702,10 @@ def verify_diff_deleted(self, config): template_info = get_dict_result(template_list, "name", templateName) - if template_info: - self.msg = "Configuration Template config is not applied to the DNAC." - self.status = "failed" - return self + if template_info: + self.msg = "Configuration Template config is not applied to the DNAC." + self.status = "failed" + return self self.log("Successfully validated absence of Template in the DNAC.") self.result.get("response").update({"Validation": "Success"}) From 7fe5e367300444762c406dd4e2f2d8873d0b25ab Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 19 Jan 2024 15:15:38 +0530 Subject: [PATCH 56/63] Update API paramters for Untagging Golden Image, fixed wired device provision issue just after device addition --- plugins/modules/inventory_intent.py | 2 +- plugins/modules/swim_intent.py | 71 ++++++++++++++++------------- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 6495e6be46..ac697ec010 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -1472,7 +1472,7 @@ def provisioned_wired_device(self): while True: response = self.get_device_response(device_ip) self.log("Device is in {0} state waiting for Managed State.".format(response['managementState'])) - if response['managementState'] == "Managed": + if response['managementState'] == "Managed" and response['collectionStatus'] == "Managed" and response["hostname"]: break response = self.dnac._exec( diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 33f37b0d50..44633f0b08 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -461,7 +461,7 @@ def site_exists(self, site_name): self.module.fail_json(msg="Site not found") if response: - log(str(response)) + self.log(str(response)) site = response.get("response") site_id = site[0].get("id") @@ -491,12 +491,12 @@ def get_image_id(self, name): params={"image_name": name}, ) - log(str(image_response)) + self.log(str(image_response)) image_list = image_response.get("response") if (len(image_list) == 1): image_id = image_list[0].get("imageUuid") - log("Image Id: " + str(image_id)) + self.log("Image Id: " + str(image_id)) else: error_message = "Image {0} not found".format(name) self.log(error_message) @@ -526,7 +526,7 @@ def is_image_exist(self, name): function='get_software_image_details', params={"image_name": name}, ) - log(str(image_response)) + self.log(str(image_response)) image_list = image_response.get("response") if (len(image_list) == 1): image_exist = True @@ -552,12 +552,12 @@ def get_device_id(self, params): function='get_device_list', params=params, ) - log(str(response)) + self.log(str(response)) device_list = response.get("response") if (len(device_list) == 1): device_id = device_list[0].get("id") - log("Device Id: " + str(device_id)) + self.log("Device Id: " + str(device_id)) else: self.log("Device not found") @@ -628,14 +628,14 @@ def get_device_family_identifier(self, family_name): family="software_image_management_swim", function='get_device_family_identifiers', ) - log(str(response)) + self.log(str(response)) device_family_db = response.get("response") if device_family_db: device_family_details = get_dict_result(device_family_db, 'deviceFamily', family_name) if device_family_details: device_family_identifier = device_family_details.get("deviceFamilyIdentifier") have["device_family_identifier"] = device_family_identifier - log("Family device indentifier:" + str(device_family_identifier)) + self.log("Family device indentifier:" + str(device_family_identifier)) else: self.module.fail_json(msg="Family Device Name not found", response=[]) self.have.update(have) @@ -675,11 +675,11 @@ def get_have(self): (site_exists, site_id) = self.site_exists(site_name) if site_exists: have["site_id"] = site_id - log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) + self.log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) else: # For global site, use -1 as siteId have["site_id"] = "-1" - log("Site Name not given by user. Using global site.") + self.log("Site Name not given by user. Using global site.") self.have.update(have) # check if given device family name exists, store indentifier value @@ -689,6 +689,14 @@ def get_have(self): if self.want.get("distribution_details"): have = {} distribution_details = self.want.get("distribution_details") + site_name = distribution_details.get("site_name") + if site_name: + site_exists = False + (site_exists, site_id) = self.site_exists(site_name) + if site_exists: + have["site_id"] = site_id + self.log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) + # check if image for distributon is available if distribution_details.get("image_name"): name = distribution_details.get("image_name").split("/")[-1] @@ -733,7 +741,7 @@ def get_have(self): (site_exists, site_id) = self.site_exists(site_name) if site_exists: have["site_id"] = site_id - log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) + self.log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) device_params = dict( hostname=activation_details.get("device_hostname"), @@ -780,7 +788,7 @@ def get_want(self, config): want["activation_details"] = config.get("image_activation_details") self.want = want - log(str(self.want)) + self.log(str(self.want)) return self @@ -820,9 +828,9 @@ def get_diff_import(self): if image_exist: image_id = self.get_image_id(name) self.have["imported_image_id"] = image_id - log_msg = "Image {0} already exists in the Cisco DNA Center".format(name) - self.result['msg'] = log_msg - self.log(log_msg) + self.msg = "Image {0} already exists in the Cisco DNA Center".format(name) + self.result['msg'] = self.msg + self.log(self.msg) self.status = "success" self.result['changed'] = False return self @@ -873,16 +881,16 @@ def get_diff_import(self): ("completed successfully" in task_details.get("progress").lower()): self.result['changed'] = True self.status = "success" - log_msg = "Swim Image {0} imported successfully".format(name) - self.result['msg'] = log_msg - self.log(log_msg) + self.msg = "Swim Image {0} imported successfully".format(name) + self.result['msg'] = self.msg + self.log(self.msg) break if task_details and task_details.get("isError"): if "already exists" in task_details.get("failureReason"): - log_msg = "SWIM Image {0} already exists in the Cisco DNA Center".format(name) - self.result['msg'] = log_msg - self.log(log_msg) + self.msg = "SWIM Image {0} already exists in the Cisco DNA Center".format(name) + self.result['msg'] = self.msg + self.log(self.msg) self.status = "success" self.result['changed'] = False break @@ -933,7 +941,7 @@ def get_diff_tagging(self): deviceFamilyIdentifier=self.have.get("device_family_identifier"), deviceRole=tagging_details.get("device_role") ) - log("Image params for tagging image as golden:" + str(image_params)) + self.log("Image params for tagging image as golden:" + str(image_params)) response = self.dnac._exec( family="software_image_management_swim", @@ -941,16 +949,16 @@ def get_diff_tagging(self): op_modifies=True, params=image_params ) - log(str(response)) + self.log(str(response)) else: image_params = dict( - imageId=self.have.get("tagging_image_id"), - siteId=self.have.get("site_id"), - deviceFamilyIdentifier=self.have.get("device_family_identifier"), - deviceRole=tagging_details.get("deviceRole") + image_id=self.have.get("tagging_image_id"), + site_id=self.have.get("site_id"), + device_family_identifier=self.have.get("device_family_identifier"), + device_role=tagging_details.get("device_role") ) - log("Image params for un-tagging image as golden:" + str(image_params)) + self.log("Image params for un-tagging image as golden:" + str(image_params)) response = self.dnac._exec( family="software_image_management_swim", @@ -958,7 +966,7 @@ def get_diff_tagging(self): op_modifies=True, params=image_params ) - log(str(response)) + self.log(str(response)) if response: task_details = {} @@ -1049,7 +1057,7 @@ def get_diff_distribution(self): imageUuid=image_id )] ) - log("Distribution Params: " + str(distribution_params)) + self.log("Distribution Params: " + str(distribution_params)) response = self.dnac._exec( family="software_image_management_swim", function='trigger_software_image_distribution', @@ -1073,6 +1081,7 @@ def get_diff_distribution(self): if task_details.get("isError"): error_msg = "Image with Id {0} Distribution Failed".format(image_id) + self.log(error_msg) self.result['response'] = task_details break @@ -1181,7 +1190,7 @@ def get_diff_activation(self): schedule_validate=activation_details.get("scehdule_validate"), payload=payload ) - log("Activation Params: " + str(activation_params)) + self.log("Activation Params: " + str(activation_params)) response = self.dnac._exec( family="software_image_management_swim", From 7cf9f3f9582b719806c1c689fbec07f784837288 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 19 Jan 2024 15:29:05 +0530 Subject: [PATCH 57/63] remove unused log --- plugins/modules/swim_intent.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 44633f0b08..39c45bb9ce 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -375,7 +375,6 @@ from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( DnacBase, validate_list_of_dicts, - log, get_dict_result, ) from ansible.module_utils.basic import AnsibleModule From 429d1ed35fa7a8eb3e7cb58251470ee6dbfb3d70 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 19 Jan 2024 16:13:42 +0530 Subject: [PATCH 58/63] Added code for handling wired provision of dump IP --- plugins/modules/inventory_intent.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index ac697ec010..d3732618e7 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -1449,7 +1449,13 @@ def provisioned_wired_device(self): """ site_name = self.config[0]['provision_wired_device']['site_name'] + device_in_dnac = self.device_exists_in_dnac() device_ips = self.config[0]['ip_address'] + + for device_ip in device_ips: + if device_ip not in device_in_dnac: + device_ips.remove(device_ip) + device_type = "Wired" provision_count, already_provision_count = 0, 0 From 54f2c44ce6a64d3489bedd25e97d7e8d110ad17e Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 19 Jan 2024 16:23:11 +0530 Subject: [PATCH 59/63] address review comments --- plugins/modules/inventory_intent.py | 7 ++++++- plugins/modules/swim_intent.py | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index d3732618e7..c1c5431085 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -1478,7 +1478,12 @@ def provisioned_wired_device(self): while True: response = self.get_device_response(device_ip) self.log("Device is in {0} state waiting for Managed State.".format(response['managementState'])) - if response['managementState'] == "Managed" and response['collectionStatus'] == "Managed" and response["hostname"]: + + if ( + response.get('managementState') == "Managed" + and response.get('collectionStatus') == "Managed" + and response.get("hostname") + ): break response = self.dnac._exec( diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index 39c45bb9ce..0996440b70 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -692,6 +692,7 @@ def get_have(self): if site_name: site_exists = False (site_exists, site_id) = self.site_exists(site_name) + if site_exists: have["site_id"] = site_id self.log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) From 59b0c699d5d6388686b29aa41aa0c18cebaa8b85 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Fri, 19 Jan 2024 13:14:21 -0500 Subject: [PATCH 60/63] line 404 sanity fix --- plugins/module_utils/dnac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index fc77f18ae5..f31eefbfa1 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -401,7 +401,7 @@ def camel_to_snake_case(self, config): return new_config -def log(msg, level='info', frameIncrement=0): +def log(msg, level='info', frameIncrement=0): with open('dnac.log', 'a') as of: callerframerecord = inspect.stack()[1 + frameIncrement] frame = callerframerecord[0] From 68fac25e5899be87688a6013fcc8e7265423cf08 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Fri, 19 Jan 2024 13:34:18 -0500 Subject: [PATCH 61/63] removed space before new line in logging statement --- plugins/module_utils/dnac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index f31eefbfa1..e6ada258f0 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -407,7 +407,7 @@ def log(msg, level='info', frameIncrement=0): frame = callerframerecord[0] info = inspect.getframeinfo(frame) d = datetime.datetime.now().replace(microsecond=0).isoformat() - of.write("---- %s ---- %s@%s ---- %s: %s \n" % (d, info.lineno, info.function, level.upper(), msg)) + of.write("---- %s ---- %s@%s ---- %s: %s\n" % (d, info.lineno, info.function, level.upper(), msg)) def is_list_complex(x): From 5454c73a27d5bfdb0c4df8550d0650192af4d3d9 Mon Sep 17 00:00:00 2001 From: Madhan Date: Sat, 20 Jan 2024 02:14:16 +0530 Subject: [PATCH 62/63] Changes in intent modules --- changelogs/changelog.yaml | 4 ++-- galaxy.yml | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 301299bea4..a68a792af6 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -764,9 +764,9 @@ releases: - To Support provisioning wired device, reboot AP's, export device list, delete provisioned devices. - Change the variable names into snake case in all the intent modules for better readability. 6.10.1: - release_date: "2023-12-22" + release_date: "2024-01-20" changes: - release_summary: Changes in network settings, site, inventory and provisioning intent modules + release_summary: Changes in network settings, site, discovery, inventory, swim, credential and provisioning intent modules minor_changes: - Introducing config_verify to verify the state operations in Catalyst Center in network settings and site intent module - Changes to support inventory and provisioning intent modules diff --git a/galaxy.yml b/galaxy.yml index 26cc840337..9ec2120a61 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,7 +1,7 @@ --- namespace: cisco name: dnac -version: 6.10.0 +version: 6.10.1 readme: README.md authors: - Rafael Campos @@ -15,6 +15,8 @@ authors: - Akash Bhaskaran - Abinash Mishra - Abhishek Maheshwari + - Phan Nguyen + - Rugvedi Kapse description: Ansible Modules for Cisco DNA Center license_file: "LICENSE" tags: From 18598b0e1b234e5e1eb781d455b7cb083b204aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Mu=C3=B1oz?= Date: Mon, 22 Jan 2024 13:24:04 -0600 Subject: [PATCH 63/63] Updating readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a4e0d7064..2e9befa1f4 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.9.0 | 2.6.0 | +| 2.3.5.3 | 6.10.1 | 2.6.0 | If your Ansible collection is older please consider updating it first.