From 8d926b8c987aee9d40e88568fc1a802b11f36091 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Sat, 11 May 2024 17:44:34 +0530 Subject: [PATCH 01/78] Fixed the problem with the slaacSupport --- .../network_settings_workflow_manager.py | 96 +++++++++---------- 1 file changed, 47 insertions(+), 49 deletions(-) diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index 7580660247..62d32646d2 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -873,60 +873,53 @@ def get_reserve_pool_params(self, pool_info): "name": pool_info.get("groupName"), "site_id": pool_info.get("siteId"), } - if len(pool_info.get("ipPools")) == 1: + pool_info_ippools = pool_info.get("ipPools") + if len(pool_info_ippools) == 1: reserve_pool.update({ - "ipv4DhcpServers": pool_info.get("ipPools")[0].get("dhcpServerIps"), - "ipv4DnsServers": pool_info.get("ipPools")[0].get("dnsServerIps"), + "ipv4DhcpServers": pool_info_ippools[0].get("dhcpServerIps"), + "ipv4DnsServers": pool_info_ippools[0].get("dnsServerIps"), "ipv6AddressSpace": "False" }) - if pool_info.get("ipPools")[0].get("gateways") != []: - reserve_pool.update({"ipv4GateWay": pool_info.get("ipPools")[0].get("gateways")[0]}) + if pool_info_ippools[0].get("gateways") != []: + reserve_pool.update({"ipv4GateWay": pool_info_ippools[0].get("gateways")[0]}) else: reserve_pool.update({"ipv4GateWay": ""}) reserve_pool.update({"ipv6AddressSpace": "False"}) - elif len(pool_info.get("ipPools")) == 2: - if not pool_info.get("ipPools")[0].get("ipv6"): - reserve_pool.update({ - "ipv4DhcpServers": pool_info.get("ipPools")[0].get("dhcpServerIps"), - "ipv4DnsServers": pool_info.get("ipPools")[0].get("dnsServerIps"), - "ipv6AddressSpace": "True", - "ipv6DhcpServers": pool_info.get("ipPools")[1].get("dhcpServerIps"), - "ipv6DnsServers": pool_info.get("ipPools")[1].get("dnsServerIps"), + elif len(pool_info_ippools) == 2: + ipv4_index = 0 + ipv6_index = 0 + if not pool_info_ippools[0].get("ipv6"): + ipv6_index += 1 + elif not pool_info_ippools[1].get("ipv6"): + ipv4_index += 1 - }) + reserve_pool.update({ + "ipv4DhcpServers": pool_info_ippools[ipv4_index].get("dhcpServerIps"), + "ipv4DnsServers": pool_info_ippools[ipv4_index].get("dnsServerIps"), + "ipv6AddressSpace": "True", + "ipv6Prefix": "True", + "ipv6DnsServers": pool_info_ippools[ipv6_index].get("dnsServerIps"), + "ipv6DhcpServers": pool_info_ippools[ipv6_index].get("dhcpServerIps") + }) + if pool_info_ippools[ipv4_index].get("gateways") != []: + reserve_pool.update({"ipv4GateWay": + pool_info_ippools[ipv4_index].get("gateways")[0]}) + else: + reserve_pool.update({"ipv4GateWay": ""}) - if pool_info.get("ipPools")[0].get("gateways") != []: - reserve_pool.update({"ipv4GateWay": - pool_info.get("ipPools")[0].get("gateways")[0]}) - else: - reserve_pool.update({"ipv4GateWay": ""}) + if pool_info_ippools[ipv6_index].get("gateways") != []: + reserve_pool.update({"ipv6GateWay": + pool_info_ippools[ipv6_index].get("gateways")[0]}) + else: + reserve_pool.update({"ipv6GateWay": ""}) - if pool_info.get("ipPools")[1].get("gateways") != []: - reserve_pool.update({"ipv6GateWay": - pool_info.get("ipPools")[1].get("gateways")[0]}) - else: - reserve_pool.update({"ipv6GateWay": ""}) - - elif not pool_info.get("ipPools")[1].get("ipv6"): - reserve_pool.update({ - "ipv4DhcpServers": pool_info.get("ipPools")[1].get("dhcpServerIps"), - "ipv4DnsServers": pool_info.get("ipPools")[1].get("dnsServerIps"), - "ipv6AddressSpace": "True", - "ipv6DnsServers": pool_info.get("ipPools")[0].get("dnsServerIps"), - "ipv6DhcpServers": pool_info.get("ipPools")[0].get("dhcpServerIps") - }) - if pool_info.get("ipPools")[1].get("gateways") != []: - reserve_pool.update({"ipv4GateWay": - pool_info.get("ipPools")[1].get("gateways")[0]}) - else: - reserve_pool.update({"ipv4GateWay": ""}) + ippools_info = pool_info_ippools[ipv6_index].get("context") + slaac_support_info = get_dict_result(ippools_info, "contextKey", "slaacSupport") + if slaac_support_info is None or slaac_support_info.get("contextValue") == "false": + reserve_pool.update({"slaacSupport": False}) + else: + reserve_pool.update({"slaacSupport": True}) - if pool_info.get("ipPools")[0].get("gateways") != []: - reserve_pool.update({"ipv6GateWay": - pool_info.get("ipPools")[0].get("gateways")[0]}) - else: - reserve_pool.update({"ipv6GateWay": ""}) - reserve_pool.update({"slaacSupport": True}) self.log("Formatted reserve pool details: {0}".format(reserve_pool), "DEBUG") return reserve_pool @@ -1528,7 +1521,8 @@ def get_want_reserve_pool(self, reserve_pool): "ipv6Subnet": item.get("ipv6_subnet"), "ipv6DnsServers": item.get("ipv6_dns_servers"), "ipv4TotalHost": item.get("ipv4_total_host"), - "ipv6TotalHost": item.get("ipv6_total_host") + "ipv6TotalHost": item.get("ipv6_total_host"), + "slaacSupport": item.get("slaac_support") } # Check for missing mandatory parameters in the playbook if pool_values.get("ipv6AddressSpace") is True: @@ -1600,13 +1594,17 @@ def get_want_reserve_pool(self, reserve_pool): if pool_values.get(key) is None: del pool_values[key] else: - keys_to_delete = ['type', 'ipv4GlobalPool', - 'ipv4Prefix', 'ipv4PrefixLength', - 'ipv4TotalHost', 'ipv4Subnet'] + keys_to_delete = ['type', 'ipv4GlobalPool', 'ipv4Prefix', 'ipv4PrefixLength', + 'ipv4TotalHost', 'ipv4Subnet', 'slaacSupport'] for key in keys_to_delete: if key in pool_values: del pool_values[key] + copy_pool_values = copy.deepcopy(pool_values) + for item in copy_pool_values: + if pool_values.get(item) is None: + del pool_values[item] + want_reserve.append(pool_values) reserve_pool_index += 1 @@ -2050,7 +2048,7 @@ def update_reserve_pool(self, reserve_pool): self.check_execution_response_status(response).check_return_status() self.log("Reserved ip subpool '{0}' updated successfully.".format(name), "INFO") result_reserve_pool.get("response") \ - .update({name: self.have.get("reservePool")[reserve_pool_index].get("details")}) + .update({name: reserve_params}) result_reserve_pool.get("response").get(name) \ .update({"Id": self.have.get("reservePool")[reserve_pool_index].get("id")}) result_reserve_pool.get("msg") \ From ce68033875152343d6995290bb48f7eace82ef74 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Sat, 11 May 2024 17:53:25 +0530 Subject: [PATCH 02/78] Resolved the sanity erros --- plugins/modules/network_settings_workflow_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index 62d32646d2..2402460523 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -908,8 +908,9 @@ def get_reserve_pool_params(self, pool_info): reserve_pool.update({"ipv4GateWay": ""}) if pool_info_ippools[ipv6_index].get("gateways") != []: - reserve_pool.update({"ipv6GateWay": - pool_info_ippools[ipv6_index].get("gateways")[0]}) + reserve_pool.update({ + "ipv6GateWay": pool_info_ippools[ipv6_index].get("gateways")[0] + }) else: reserve_pool.update({"ipv6GateWay": ""}) From 4357b87cb7134ef541c85e198f31eda1f7d92eb0 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 14 May 2024 15:15:30 +0530 Subject: [PATCH 03/78] Addressed the review comments --- .../network_settings_workflow_manager.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index 2402460523..f00e4dd6ee 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -204,7 +204,9 @@ description: IPv6 prefix length is required when the ipv6_prefix value is true. type: int ipv6_total_host: - description: The total number of hosts for IPv6 is required if the 'ipv6_prefix' is set to false. + description: + - The total number of hosts for IPv6 is required if the 'ipv6_prefix' is set to false. + - The number of IP addresses for IPv6 should be less than 256. type: int prev_name: description: The former name associated with the reserved IP sub-pool. @@ -874,7 +876,8 @@ def get_reserve_pool_params(self, pool_info): "site_id": pool_info.get("siteId"), } pool_info_ippools = pool_info.get("ipPools") - if len(pool_info_ippools) == 1: + pool_info_length = len(pool_info_ippools) + if pool_info_length == 1: reserve_pool.update({ "ipv4DhcpServers": pool_info_ippools[0].get("dhcpServerIps"), "ipv4DnsServers": pool_info_ippools[0].get("dnsServerIps"), @@ -885,13 +888,13 @@ def get_reserve_pool_params(self, pool_info): else: reserve_pool.update({"ipv4GateWay": ""}) reserve_pool.update({"ipv6AddressSpace": "False"}) - elif len(pool_info_ippools) == 2: - ipv4_index = 0 - ipv6_index = 0 + else: if not pool_info_ippools[0].get("ipv6"): - ipv6_index += 1 - elif not pool_info_ippools[1].get("ipv6"): - ipv4_index += 1 + ipv4_index = 0 + ipv6_index = 1 + else: + ipv4_index = 1 + ipv6_index = 0 reserve_pool.update({ "ipv4DhcpServers": pool_info_ippools[ipv4_index].get("dhcpServerIps"), From 0c24da9a4cdc0a97c3af0fd80b8f7fe8e08136e5 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 14 May 2024 15:37:05 +0530 Subject: [PATCH 04/78] Addressed the review comments --- .../network_settings_workflow_manager.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index f00e4dd6ee..1692320ecc 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -197,16 +197,17 @@ type: str ipv6_prefix: description: > - Ipv6 prefix value is true, the ip6 prefix length input field is enabled, - if it is false ipv6 total Host input is enable. + Determines whether to enable the 'ipv6_prefix_length' or 'ipv6_total_host' input field. + If IPv6 prefix value is true, the IPv6 prefix length input field is mandatory, + If it is false ipv6 total Host input is mandatory. type: bool ipv6_prefix_length: - description: IPv6 prefix length is required when the ipv6_prefix value is true. + description: Specifies the IPv6 prefix length. Required when 'ipv6_prefix' is set to true. type: int ipv6_total_host: description: - - The total number of hosts for IPv6 is required if the 'ipv6_prefix' is set to false. - - The number of IP addresses for IPv6 should be less than 256. + - Specifies the total number of IPv6 hosts. Required when 'ipv6_prefix' is set to false. + - Must specify a number of IPv6 IP addresses that is less than or equal to 256. type: int prev_name: description: The former name associated with the reserved IP sub-pool. @@ -877,6 +878,9 @@ def get_reserve_pool_params(self, pool_info): } pool_info_ippools = pool_info.get("ipPools") pool_info_length = len(pool_info_ippools) + + # If the reserved pool has only IPv4, pool_info_length will be 1. + # If the reserved pool has both IPv4 and IPv6, pool_info_length will be 2. if pool_info_length == 1: reserve_pool.update({ "ipv4DhcpServers": pool_info_ippools[0].get("dhcpServerIps"), @@ -889,6 +893,9 @@ def get_reserve_pool_params(self, pool_info): reserve_pool.update({"ipv4GateWay": ""}) reserve_pool.update({"ipv6AddressSpace": "False"}) else: + + # If the ipv6 flag is set in the second element, ipv4_index will be 0 and ipv6_index will be 1. + # If the ipv6 flag is set in the first element, ipv4_index will be 1 and ipv6_index will be 0. if not pool_info_ippools[0].get("ipv6"): ipv4_index = 0 ipv6_index = 1 From f7f403c80e257de8b4f74584787efa047bd09343 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 14 May 2024 15:49:20 +0530 Subject: [PATCH 05/78] Addressed the review comments --- plugins/modules/network_settings_workflow_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index 1692320ecc..b7c3563560 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -198,8 +198,8 @@ ipv6_prefix: description: > Determines whether to enable the 'ipv6_prefix_length' or 'ipv6_total_host' input field. - If IPv6 prefix value is true, the IPv6 prefix length input field is mandatory, - If it is false ipv6 total Host input is mandatory. + If IPv6 prefix value is true, the IPv6 prefix length input field is required, + If it is false ipv6 total Host input is required. type: bool ipv6_prefix_length: description: Specifies the IPv6 prefix length. Required when 'ipv6_prefix' is set to true. @@ -207,7 +207,7 @@ ipv6_total_host: description: - Specifies the total number of IPv6 hosts. Required when 'ipv6_prefix' is set to false. - - Must specify a number of IPv6 IP addresses that is less than or equal to 256. + - Must specify a number of IPv6 IP addresses that is less than 256. type: int prev_name: description: The former name associated with the reserved IP sub-pool. From 26aa51d7118316e96d6aa75c5a3901c7e7abc315 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 15 May 2024 10:38:46 +0530 Subject: [PATCH 06/78] Fixed the parameter confusion between the ipAddress and network for ISE --- .../modules/network_settings_workflow_manager.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index b7c3563560..3f6acb420f 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -1050,9 +1050,9 @@ def get_network_params(self, site_id): else: network_settings.update({ "network_aaa": { - "network": aaa_value.get("ipAddress"), + "network": aaa_pan_value, "protocol": aaa_value.get("protocol"), - "ipAddress": aaa_pan_value, + "ipAddress": aaa_value.get("ipAddress"), "servers": "ISE" } }) @@ -1072,9 +1072,9 @@ def get_network_params(self, site_id): else: network_settings.update({ "clientAndEndpoint_aaa": { - "network": aaa_value.get("ipAddress"), + "network": aaa_pan_value, "protocol": aaa_value.get("protocol"), - "ipAddress": aaa_pan_value, + "ipAddress": aaa_value.get("ipAddress"), "servers": "ISE" } }) @@ -1669,13 +1669,14 @@ def get_want_network(self, network_management_details): else: del want_network_settings["ntpServer"] + have_timezone = self.have.get("network").get("net_details").get("settings").get("timezone") if network_management_details.get("timezone") is not None: want_network_settings["timezone"] = \ network_management_details.get("timezone") + elif have_timezone is not None: + want_network_settings["timezone"] = have_timezone else: - self.msg = "missing parameter timezone in network" - self.status = "failed" - return self + want_network_settings["timezone"] = "GMT" dnsServer = network_management_details.get("dns_server") if dnsServer is not None: From b25e021d232c792c216e631f9c3b3508d286af01 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 15 May 2024 11:49:40 +0530 Subject: [PATCH 07/78] Changed the netflow_collector port unavailability from None to 'null' --- .../modules/network_settings_workflow_manager.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index 3f6acb420f..5c745996d0 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -991,10 +991,6 @@ def get_network_params(self, site_id): "configureDnacIP": syslog_details.get("value")[0].get("configureDnacIP"), "ipAddresses": syslog_details.get("value")[0].get("ipAddresses"), }, - "netflowcollector": { - "ipAddress": netflow_details.get("value")[0].get("ipAddress"), - "port": netflow_details.get("value")[0].get("port") - }, "timezone": timezone_details.get("value")[0], } } @@ -1018,6 +1014,18 @@ def get_network_params(self, site_id): else: network_settings.update({"ntpServer": [""]}) + netflowcollector_values = netflow_details.get("value")[0] + ipAddress = netflowcollector_values.get("ipAddress") + port = netflowcollector_values.get("port") + if port is None: + port = "null" + network_settings.update({ + "netflowcollector": { + "ipAddress": ipAddress, + "port": port, + } + }) + if messageoftheday_details is not None: network_settings.update({ "messageOfTheday": { From 840593c912bfa6a823bda141bf9667868aeac3b8 Mon Sep 17 00:00:00 2001 From: Abinash Date: Wed, 15 May 2024 06:29:10 +0000 Subject: [PATCH 08/78] PR for assigning device to site --- playbooks/device_provision_workflow.yml | 18 +- plugins/modules/provision_workflow_manager.py | 342 ++++++++++++++++-- 2 files changed, 324 insertions(+), 36 deletions(-) diff --git a/playbooks/device_provision_workflow.yml b/playbooks/device_provision_workflow.yml index acb3249a54..0e6e631331 100644 --- a/playbooks/device_provision_workflow.yml +++ b/playbooks/device_provision_workflow.yml @@ -16,18 +16,30 @@ dnac_port: "{{ dnac_port }}" dnac_version: "{{ dnac_version }}" dnac_debug: "{{ dnac_debug }}" + dnac_log_level: "{{ dnac_log_level }}" tasks: - - name: Provision a wired device to a site + - name: Assign a wired device to a site cisco.dnac.provision_workflow_manager: <<: *dnac_login dnac_log: True state: merged config_verify: True config: - - site_name_hierarchy: Global/USA/San Francisco/BGL_18 - management_ip_address: 204.1.2.2 + - site_name_hierarchy: Global/India/Bangalore/Mantri Square + management_ip_address: 204.192.3.40 + provisioning: false + - name: Provision a wired device to a site + cisco.dnac.provision_workflow_manager: + <<: *dnac_login + dnac_log: True + dnac_log_level: DEBUG + state: merged + config_verify: True + config: + - site_name_hierarchy: Global/USA/SAN JOSE/BLD23/BLD20_FLOOR2 + management_ip_address: 204.192.3.40 - name: Unprovision a wired device from a site cisco.dnac.provision_workflow_manager: diff --git a/plugins/modules/provision_workflow_manager.py b/plugins/modules/provision_workflow_manager.py index 1bd3aa5d68..fed4db877c 100644 --- a/plugins/modules/provision_workflow_manager.py +++ b/plugins/modules/provision_workflow_manager.py @@ -39,9 +39,17 @@ required: true suboptions: management_ip_address: - description: Management Ip Address . + description: Management Ip Address of the device. type: str required: true + provisioning: + description: + - Describes whesther the user wants to perform only site assignment or provisioning of a wired device. + - False, for site assignment only + - True, for provisioning to a site + type: bool + required: false + default: true site_name_hierarchy: description: Name of site where the device needs to be added. type: str @@ -95,10 +103,12 @@ post /dna/intent/api/v1/business/sda/provision-device post /dna/intent/api/v1/wireless/provision + - Added 'provisioning' option in v6.13.2 + """ EXAMPLES = r""" -- name: Create/Modify a new provision +- name: Provision a wireless device to a site cisco.dnac.provision_workflow_manager: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" @@ -110,16 +120,46 @@ dnac_log: True state: merged config: - - site_name_hierarchy: string - management_ip_address: string - managed_ap_locations: list + - site_name_hierarchy: Global/USA/San Francisco/BGL_18 + management_ip_address: 204.192.3.40 + managed_ap_locations: + - Global/USA/San Francisco/BGL_18/Test_Floor2 dynamic_interfaces: - - vlan_id: integer - interface_name: string - interface_ip_address: string - interface_gateway: string - interface_netmask_in_c_i_d_r: integer - lag_or_port_number: integer + - vlan_id: 1866 + interface_name: Vlan1866 + interface_ip_address: 204.192.6.200 + interface_gateway: 204.192.6.1 + +- name: Provision a wired device to a site + cisco.dnac.provision_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + state: merged + config: + - site_name_hierarchy: Global/USA/San Francisco/BGL_18 + management_ip_address: 204.192.3.40 + +- name: Assign a wired device to a site + cisco.dnac.provision_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + state: merged + config: + - site_name_hierarchy: Global/USA/San Francisco/BGL_18 + management_ip_address: 204.192.3.40 + provisioning: false """ @@ -210,7 +250,8 @@ def validate_input(self, state=None): "managed_ap_locations": {'type': 'list', 'required': False, 'elements': 'str'}, "dynamic_interfaces": {'type': 'list', 'required': False, - 'elements': 'dict'} + 'elements': 'dict'}, + "provisioning": {'type': 'bool', 'required': False, "default": True} } if state == "merged": provision_spec["site_name_hierarchy"] = {'type': 'str', 'required': True} @@ -268,6 +309,36 @@ def get_dev_type(self): self.log("The device type is {0}".format(device_type), "INFO") return device_type + def get_serial_number(self): + """ + Fetches the serial number of the device + + 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: + - serial_number: A string indicating the serial number of the device + Example: + Post creation of the validated input, we this method gets the + serial number of the device. + """ + + response = self.dnac_apply['exec']( + family="devices", + function='get_network_device_by_ip', + params={"ip_address": self.validated_config[0]["management_ip_address"]}, + op_modifies=True + ) + + self.log("The device response from 'get_network_device_by_ip' API is {0}".format(str(response)), "DEBUG") + dev_dict = response.get("response") + serial_number = dev_dict.get("serialNumber") + + self.log("Serial Number of the device is {0}".format(str(serial_number)), "INFO") + + return serial_number + def get_task_status(self, task_id=None): """ Fetches the status of the task once any provision API is called @@ -303,7 +374,7 @@ def get_task_status(self, task_id=None): self.module.fail_json(msg=msg) return False - if response.get('progress') != 'In Progress': + if response.get('progress') == 'TASK_PROVISION' and response.get("isError") is False: result = True break @@ -311,6 +382,48 @@ def get_task_status(self, task_id=None): self.result.update(dict(provision_task=response)) return result + def get_execution_status(self, execution_id=None): + """ + Fetches the status of the task once any provision API is called + + 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: + - result: A dict indiacting wheter the task was succesful or not + Example: + Post creation of the provision task, this method fetheches the task + status. + + """ + result = False + params = {"execution_id": execution_id} + while True: + response = self.dnac_apply['exec']( + family="task", + function="get_business_api_execution_details", + params=params, + op_modifies=True + ) + self.log("Response collected from 'get_business_api_execution_details' API is {0}".format(str(response)), "DEBUG") + self.log("Execution status for the execution id {0} is {1}".format(str(execution_id), str(response.get("status"))), "INFO") + if response.get('bapiError') or re.search( + 'failed', response.get('bapiError'), flags=re.IGNORECASE + ): + msg = 'Assigning to site execution with id {0} has not completed - Reason: {1}'.format( + execution_id, response.get("bapiError")) + self.module.fail_json(msg=msg) + return False + + if response.get('status') == 'SUCCESS': + result = True + break + + time.sleep(3) + self.result.update(dict(assignment_task=response)) + return result + def get_site_type(self, site_name_hierarchy=None): """ Fetches the type of site @@ -351,6 +464,85 @@ def get_site_type(self, site_name_hierarchy=None): return site_type + def get_site_details(self, site_name_hierarchy=None): + """ + Fetches the id and existance of the site + + 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: + - site_id: A string indicating the id of the site. + - site_exits: A boolean value indicating the existance of the site. + Example: + Post creation of the validated input, we this method gets the + id of the site. + """ + + site_exists = False + site_id = None + try: + response = self.dnac_apply['exec']( + family="sites", + function='get_site', + params={"name": site_name_hierarchy}, + op_modifies=True + ) + except Exception: + self.log("Exception occurred as \ + site '{0}' was not found".format(self.want.get("site_name")), "CRITICAL") + self.module.fail_json(msg="Site not found", response=[]) + + if response: + self.log("Received site details\ + for '{0}': {1}".format(site_name_hierarchy, str(response)), "DEBUG") + site = response.get("response") + site_additional_info = site[0].get("additionalInfo") + if len(site) == 1: + site_id = site[0].get("id") + site_exists = True + self.log("Site Name: {1}, Site ID: {0}".format(site_id, site_name_hierarchy), "INFO") + + return (site_exists, site_id) + + def get_site_assignment(self): + """ + Fetches the details of devices assigned to a site + + 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: + - boolean: True if any device is associated with the site, False if no device is associated with site + + Example: + Post creation of the validated input, this method telss whether devices are associated with a site. + """ + + site_name_hierarchy = self.validated_config[0].get("site_name_hierarchy") + site_exits, site_id = self.get_site_details(site_name_hierarchy=site_name_hierarchy) + serial_number = self.get_serial_number() + if site_exits: + site_response = self.dnac_apply['exec']( + family="sites", + function='get_membership', + params={"site_id": site_id, + "serial_number": serial_number}, + op_modifies=True + ) + self.log("Response collected from the 'get_memership' API is {0}".format(site_response), "DEBUG") + device_list = site_response.get("device") + if len(device_list) > 0: + if all(device.get("response") == [] for device in device_list): + return False + else: + return True + else: + return False + return False + def get_wired_params(self): """ Prepares the payload for provisioning of the wired devices @@ -369,10 +561,29 @@ def get_wired_params(self): parameters in other APIs. """ - wired_params = { - "deviceManagementIpAddress": self.validated_config[0]["management_ip_address"], - "siteNameHierarchy": self.validated_config[0].get("site_name_hierarchy") - } + site_name = self.validated_config[0].get("site_name_hierarchy") + + (site_exits, site_id) = self.get_site_details(site_name_hierarchy=site_name) + + if site_exits is False: + msg = "Site {0} doesn't exist".format(site_name) + self.log(msg, "CRITICAL") + self.module.fail_json(msg=msg) + + if self.validated_config[0].get("provisioning") is True: + wired_params = { + "deviceManagementIpAddress": self.validated_config[0]["management_ip_address"], + "siteNameHierarchy": site_name + } + else: + wired_params = { + "device": [ + { + "ip": self.validated_config[0]["management_ip_address"] + } + ], + "site_id": site_id + } self.log("Parameters collected for the provisioning of wired device:{0}".format(wired_params), "INFO") return wired_params @@ -495,35 +706,91 @@ class instance for further use. self.log("The provisioned status of the wired device is {0}".format(status), "INFO") if status == "success": - response = self.dnac_apply['exec']( - family="sda", - function="re_provision_wired_device", - op_modifies=True, - params=self.want["prov_params"], - ) - self.log("Reprovisioning response collected from 're_provision_wired_device' API is: {0}".format(response), "DEBUG") + try: + response = self.dnac_apply['exec']( + family="sda", + function="re_provision_wired_device", + op_modifies=True, + params=self.want["prov_params"], + ) + self.log("Reprovisioning response collected from 're_provision_wired_device' API is: {0}".format(response), "DEBUG") + task_id = response.get("taskId") + provision_info = self.get_task_status(task_id=task_id) + self.result["changed"] = True + self.result['msg'] = "Re-Provision done Successfully" + self.result['diff'] = self.validated_config + self.result['response'] = task_id + self.log(self.result['msg'], "INFO") + return self + + except Exception as e: + self.msg = "Error in re-provisioning due to {0}".format(str(e)) + self.log(self.msg, "ERROR") + self.status = "failed" + return self else: - response = self.dnac_apply['exec']( - family="sda", - function="provision_wired_device", - op_modifies=True, - params=self.want["prov_params"], - ) - self.log("Provisioning response collected from 'provision_wired_device' API is: {0}".format(response), "DEBUG") + if self.validated_config[0].get("provisioning") is True: + try: + response = self.dnac_apply['exec']( + family="sda", + function="provision_wired_device", + op_modifies=True, + params=self.want["prov_params"], + ) + self.log("Provisioning response collected from 'provision_wired_device' API is: {0}".format(response), "DEBUG") + except Exception as e: + self.msg = "Error in provisioning due to {0}".format(str(e)) + self.log(self.msg, "ERROR") + self.status = "failed" + return self + + else: + if self.get_site_assignment() is True: + self.result["changed"] = False + self.result['msg'] = "Device is already assigned to the desired site" + self.result['diff'] = self.want + self.result['response'] = self.want.get("prov_params").get("site_id") + self.log(self.result['msg'], "INFO") + return self + + try: + response = self.dnac_apply['exec']( + family="sites", + function="assign_devices_to_site", + op_modifies=True, + params={ + "site_id": self.want.get("prov_params").get("site_id"), + "payload": self.want.get("prov_params") + }, + ) + self.log("Assignment response collected from 'assign_devices_to_site' API is: {0}".format(response), "DEBUG") + execution_id = response.get("executionId") + assignment_info = self.get_execution_status(execution_id=execution_id) + self.result["changed"] = True + self.result['msg'] = "Site assignment done successfully" + self.result['diff'] = self.validated_config + self.result['response'] = execution_id + self.log(self.result['msg'], "INFO") + return self + except Exception as e: + self.msg = "Error in site assignment due to {0}".format(str(e)) + self.log(self.msg, "ERROR") + self.status = "failed" + return self elif device_type == "wireless": response = self.dnac_apply['exec']( family="wireless", function="provision", op_modifies=True, - params=self.want["prov_params"], + params=self.want.get("prov_params"), ) self.log("Wireless provisioning response collected from 'provision' API is: {0}".format(response), "DEBUG") else: self.result['msg'] = "Passed device is neither wired nor wireless" self.log(self.result['msg'], "ERROR") - self.result['response'] = self.want["prov_params"] + self.result['response'] = self.want.get("prov_params") return self task_id = response.get("taskId") @@ -615,6 +882,15 @@ def verify_diff_merged(self): # Code to validate Cisco Catalyst Center config for merged state device_type = self.want.get("device_type") + provisioning = self.validated_config[0].get("provisioning") + site_name_hierarchy = self.validated_config[0].get("site_name_hierarchy") + if provisioning is False: + if self.get_site_assignment() is True: + self.log("Requested device is already added to the site {0}".format(site_name_hierarchy), "INFO") + else: + self.log("Requested device is not added to the site {0}".format(site_name_hierarchy), "INFO") + return self + if device_type == "wired": try: status_response = self.dnac_apply['exec']( From 6a323e791571499a3267adc4959f575c3c87fe6a Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 15 May 2024 12:03:30 +0530 Subject: [PATCH 09/78] Addressed the review comments --- plugins/modules/network_settings_workflow_manager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index 5c745996d0..9d3e395b9a 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -1014,14 +1014,15 @@ def get_network_params(self, site_id): else: network_settings.update({"ntpServer": [""]}) - netflowcollector_values = netflow_details.get("value")[0] - ipAddress = netflowcollector_values.get("ipAddress") - port = netflowcollector_values.get("port") + netflow_collector_values = netflow_details.get("value")[0] + ip_address = netflow_collector_values.get("ipAddress") + port = netflow_collector_values.get("port") if port is None: port = "null" + network_settings.update({ "netflowcollector": { - "ipAddress": ipAddress, + "ipAddress": ip_address, "port": port, } }) From c0fe7f1e6bdd818e011f7391a60ca8851be3dcdb Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 15 May 2024 17:35:40 +0530 Subject: [PATCH 10/78] Added clear description for the documentation for the network functions --- .../network_settings_workflow_manager.py | 182 ++++++++++++------ 1 file changed, 118 insertions(+), 64 deletions(-) diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index 9d3e395b9a..2d7be3c585 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -182,7 +182,7 @@ type: str ipv6_global_pool: description: - - The ipv6_global_pool is a mandatory when the ipv6_address_space is set to true. + - The ipv6_global_pool is a required when the ipv6_address_space is set to true. - It specifies the global IPv6 address pool using CIDR notation, such as "2001:db8:85a3::/64". - In cases where both ipv6_global_pool and ipv6_global_pool_name are specified, ipv6_global_pool will take precedence. type: str @@ -224,59 +224,86 @@ site_name: description: > The name of the site provided as a path parameter, used - to specify where the IP sub-pool will be reserved. + to specify where the IP sub-pool will be reserved. (eg Global/Chennai/Trill) type: str settings: description: Network management details settings. type: dict suboptions: network_aaa: - description: Network V2's network_aaa. + description: Manages AAA (Authentication Authorization Accounting) for network devices. suboptions: + servers: + description: Server type for managing AAA for network devices. + choices: [AAA, ISE] + default: ISE + type: str ip_address: - description: IP address for AAA and ISE server (eg 1.1.1.1). + description: + - Primary IP address for the ISE server. + - Secondary IP address for the AAA server. + - For example, 1.1.1.11. type: str network: - description: IP Address for AAA or ISE server (eg 2.2.2.2). + description: + - PAN IP address for the ISE server. + - Primary IP address for the AAA server. + - For example, 1.1.1.10. type: str protocol: - description: Protocol for AAA or ISE serve (eg RADIUS). - type: str - servers: - description: Server type for AAA Network (eg AAA). + description: Protocol for AAA or ISE server. + choices: [RADIUS, TACACS] + default: RADIUS type: str shared_secret: - description: Shared secret for ISE Server. + description: + - Shared secret for ISE Server. + - Required when the servers is set to ISE. + - Length of the shared secret should be atleast 4 characters. type: str type: dict client_and_endpoint_aaa: - description: Network V2's clientAndEndpoint_aaa. + description: Manages AAA (Authentication Authorization Accounting) for clients and endpoints. suboptions: + servers: + description: + - Server type for managing AAA for client and endpoints. + choices: [AAA, ISE] + default: ISE + type: str ip_address: - description: IP address for ISE serve (eg 1.1.1.4). + description: + - Primary IP address for the ISE server. + - Secondary IP address for the AAA server. + - For example, 1.1.1.1. type: str network: - description: IP address for AAA or ISE server (eg 2.2.2.1). + description: + - PAN IP address for the ISE server. + - Primary IP address for the AAA server. + - For example, 1.1.1.2. type: str protocol: - description: Protocol for AAA or ISE serve (eg RADIUS). - type: str - servers: - description: Server type AAA or ISE server (eg AAA). + description: Protocol for AAA or ISE server. + choices: [RADIUS, TACACS] + default: RADIUS type: str shared_secret: - description: Shared secret for ISE server. + description: + - Shared secret for ISE Server. + - Required when the servers is set to ISE. + - Length of the shared secret should be atleast 4 characters. type: str type: dict dhcp_server: - description: DHCP Server IP (eg 1.1.1.1). + description: DHCP Server IP address (eg 1.1.1.4). elements: str type: list dns_server: - description: Network V2's dnsServer. + description: DNS server details of the network under a specific site. suboptions: domain_name: - description: Domain Name of DHCP (eg; cisco). + description: Domain Name of DHCP (eg; cisco.com, cisco.net). type: str primary_ip_address: description: Primary IP Address for DHCP (eg 2.2.2.2). @@ -286,24 +313,24 @@ type: str type: dict ntp_server: - description: IP address for NTP server (eg 1.1.1.2). + description: IP address for NTP server under a specific site (eg 1.1.1.2). elements: str type: list timezone: - description: Input for time zone (eg Africa/Abidjan). + description: Time zone of a specific site. (eg Africa/Abidjan/GMT). type: str message_of_the_day: - description: Network V2's messageOfTheday. + description: Banner details under a specific site. suboptions: banner_message: - description: Massage for Banner message (eg; Good day). + description: Message for the banner (eg; Good day). type: str retain_existing_banner: - description: Retain existing Banner Message (eg "true" or "false"). - type: str + description: Retain existing banner message. + type: bool type: dict netflow_collector: - description: Network V2's netflowcollector. + description: Netflow collector details under a specific site. suboptions: ip_address: description: IP Address for NetFlow collector (eg 3.3.3.1). @@ -313,7 +340,7 @@ type: int type: dict snmp_server: - description: Network V2's snmpServer. + description: Snmp Server details under a specific site. suboptions: configure_dnac_ip: description: Configuration Cisco Catalyst Center IP for SNMP Server (eg true). @@ -324,7 +351,7 @@ type: list type: dict syslog_server: - description: Network V2's syslogServer. + description: syslog Server details under a specific site. suboptions: configure_dnac_ip: description: Configuration Cisco Catalyst Center IP for syslog server (eg true). @@ -478,13 +505,13 @@ site_name: string settings: network_aaa: + servers: string network: string protocol: string - servers: string client_and_endpoint_aaa: + servers: string network: string protocol: string - servers: string dhcp_server: list dns_server: domain_name: string @@ -494,7 +521,7 @@ timezone: string message_of_the_day: banner_message: string - retain_existing_banner: string + retain_existing_banner: bool netflow_collector: ip_address: string port: 443 @@ -1235,7 +1262,7 @@ def get_have_global_pool(self, global_pool_details): for pool_details in global_pool_ippool: name = pool_details.get("name") if name is None: - self.msg = "Mandatory Parameter name '{0}' required for global pool".format(name) + self.msg = "Missing required parameter 'name' in global_pool_details" self.status = "failed" return self @@ -1277,7 +1304,7 @@ def get_have_reserve_pool(self, reserve_pool_details): for item in reserve_pool_details: name = item.get("name") if name is None: - self.msg = "Mandatory Parameter name required in reserve_pool_details." + self.msg = "Missing required parameter 'name' in reserve_pool_details." self.status = "failed" return self site_name = item.get("site_name") @@ -1405,7 +1432,7 @@ def get_global_pool_cidr(self, global_pool_cidr, global_pool_name): return global_pool_cidr if not global_pool_name: - self.msg = "Missing parameter 'Global Pool CIDR' or 'Global Pool name' is mandatory under reserve_pool_details." + self.msg = "Missing parameter 'Global Pool CIDR' or 'Global Pool name' is required under reserve_pool_details." self.status = "failed" return self.check_return_status() @@ -1544,14 +1571,14 @@ def get_want_reserve_pool(self, reserve_pool): "ipv6TotalHost": item.get("ipv6_total_host"), "slaacSupport": item.get("slaac_support") } - # Check for missing mandatory parameters in the playbook + # Check for missing required parameters in the playbook if pool_values.get("ipv6AddressSpace") is True: pool_values.update({ "ipv6GlobalPool": self.get_global_pool_cidr(item.get("ipv6_global_pool"), item.get("ipv6_global_pool_name"))}) if not pool_values.get("name"): - self.msg = "Missing mandatory parameter 'name' in reserve_pool_details '{0}' element" \ + self.msg = "Missing required parameter 'name' in reserve_pool_details '{0}' element" \ .format(reserve_pool_index + 1) self.status = "failed" return self @@ -1757,14 +1784,21 @@ def get_want_network(self, network_management_details): "bannerMessage": messageOfTheday.get("banner_message") }) - if messageOfTheday.get("retain_existing_banner") is not None: - want_network_settings.get("messageOfTheday").update({ - "retainExistingBanner": - messageOfTheday.get("retain_existing_banner") - }) + retain_existing_banner = messageOfTheday.get("retain_existing_banner") + if retain_existing_banner is not None: + if retain_existing_banner is True: + want_network_settings.get("messageOfTheday").update({ + "retainExistingBanner": "true" + }) + else: + want_network_settings.get("messageOfTheday").update({ + "retainExistingBanner": "false" + }) else: del want_network_settings["messageOfTheday"] + server_types = ["AAA", "ISE"] + protocol_types = ["RADIUS", "TACACS"] network_aaa = network_management_details.get("network_aaa") if network_aaa: if network_aaa.get("ip_address"): @@ -1787,23 +1821,33 @@ def get_want_network(self, network_management_details): self.status = "failed" return self - if network_aaa.get("protocol"): + protocol = network_aaa.get("protocol") + if protocol: want_network_settings.get("network_aaa").update({ - "protocol": - network_aaa.get("protocol") + "protocol": protocol }) else: - self.msg = "missing parameter protocol in network_aaa" + want_network_settings.get("network_aaa").update({ + "protocol": "RADIUS" + }) + + if protocol not in protocol_types: + self.msg = "The 'protocol' in the network_aaa should be in {0}".format(protocol_types) self.status = "failed" return self - if network_aaa.get("servers"): + servers = network_aaa.get("servers") + if servers: want_network_settings.get("network_aaa").update({ - "servers": - network_aaa.get("servers") + "servers": servers }) else: - self.msg = "missing parameter servers in network_aaa" + want_network_settings.get("network_aaa").update({ + "servers": "ISE" + }) + + if servers not in server_types: + self.msg = "The 'servers' in the network_aaa should be in {0}".format(server_types) self.status = "failed" return self @@ -1817,13 +1861,28 @@ def get_want_network(self, network_management_details): clientAndEndpoint_aaa = network_management_details.get("client_and_endpoint_aaa") if clientAndEndpoint_aaa: + servers = clientAndEndpoint_aaa.get("servers") + if servers: + want_network_settings.get("clientAndEndpoint_aaa").update({ + "servers": servers + }) + else: + want_network_settings.get("clientAndEndpoint_aaa").update({ + "servers": "ISE" + }) + + if servers not in server_types: + self.msg = "The 'servers' in the client_and_endpoint_aaa should be in {0}".format(server_types) + self.status = "failed" + return self + if clientAndEndpoint_aaa.get("ip_address"): want_network_settings.get("clientAndEndpoint_aaa").update({ "ipAddress": clientAndEndpoint_aaa.get("ip_address") }) else: - if clientAndEndpoint_aaa.get("servers") == "ISE": + if servers == "ISE": self.msg = "Failed to process client_and_endpoint_aaa due to missing 'ip_address' parameter. ISE server is configured." self.status = "failed" return self @@ -1838,23 +1897,18 @@ def get_want_network(self, network_management_details): self.status = "failed" return self - if clientAndEndpoint_aaa.get("protocol"): + protocol = clientAndEndpoint_aaa.get("protocol") + if protocol: want_network_settings.get("clientAndEndpoint_aaa").update({ - "protocol": - clientAndEndpoint_aaa.get("protocol") + "protocol": protocol }) else: - self.msg = "Failed to process client_and_endpoint_aaa due to missing parameter 'protocol' in the playbook." - self.status = "failed" - return self - - if clientAndEndpoint_aaa.get("servers"): want_network_settings.get("clientAndEndpoint_aaa").update({ - "servers": - clientAndEndpoint_aaa.get("servers") + "protocol": "RADIUS" }) - else: - self.msg = "Failed to process client_and_endpoint_aaa due to missing parameter 'servers' in the playbook." + + if protocol not in protocol_types: + self.msg = "The 'protocol' in the client_and_endpoint_aaa should be in {0}".format(protocol_types) self.status = "failed" return self From 2c3d7ec701e19e409e0cc2901e02fdd1e7baf678 Mon Sep 17 00:00:00 2001 From: Abinash Date: Thu, 16 May 2024 05:41:52 +0000 Subject: [PATCH 11/78] PR for assigning device to site --- plugins/modules/provision_workflow_manager.py | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/plugins/modules/provision_workflow_manager.py b/plugins/modules/provision_workflow_manager.py index fed4db877c..981fbbb0cb 100644 --- a/plugins/modules/provision_workflow_manager.py +++ b/plugins/modules/provision_workflow_manager.py @@ -44,9 +44,9 @@ required: true provisioning: description: - - Describes whesther the user wants to perform only site assignment or provisioning of a wired device. - - False, for site assignment only - - True, for provisioning to a site + - Specifies whether the user intends to perform site assignment only or full provisioning for a wired device. + - Set to 'False' to carry out site assignment only. + - Set to 'True' to proceed with provisioning to a site. type: bool required: false default: true @@ -103,7 +103,7 @@ post /dna/intent/api/v1/business/sda/provision-device post /dna/intent/api/v1/wireless/provision - - Added 'provisioning' option in v6.13.2 + - Added 'provisioning' option in v6.14.1 """ @@ -159,7 +159,7 @@ config: - site_name_hierarchy: Global/USA/San Francisco/BGL_18 management_ip_address: 204.192.3.40 - provisioning: false + provisioning: False """ @@ -320,21 +320,34 @@ def get_serial_number(self): The method returns an instance of the class with updated attributes: - serial_number: A string indicating the serial number of the device Example: - Post creation of the validated input, we this method gets the + After creating the validated input, this method retrieves the serial number of the device. """ - response = self.dnac_apply['exec']( - family="devices", - function='get_network_device_by_ip', - params={"ip_address": self.validated_config[0]["management_ip_address"]}, - op_modifies=True - ) + try: + response = self.dnac_apply['exec']( + family="devices", + function='get_network_device_by_ip', + params={"ip_address": self.validated_config[0]["management_ip_address"]}, + op_modifies=True + ) + + except Exception as e: + self.log("An error occurred while fetching the serial number: {0}".format(str(e)), "ERROR") + return None + + if not (response or response.get("response")): + self.log("No response received from 'get_network_device_by_ip' API or it's invalid.", "ERROR") + return None self.log("The device response from 'get_network_device_by_ip' API is {0}".format(str(response)), "DEBUG") dev_dict = response.get("response") serial_number = dev_dict.get("serialNumber") + if not serial_number: + self.log("Serial number not found in the response.", "ERROR") + return None + self.log("Serial Number of the device is {0}".format(str(serial_number)), "INFO") return serial_number @@ -408,9 +421,7 @@ def get_execution_status(self, execution_id=None): ) self.log("Response collected from 'get_business_api_execution_details' API is {0}".format(str(response)), "DEBUG") self.log("Execution status for the execution id {0} is {1}".format(str(execution_id), str(response.get("status"))), "INFO") - if response.get('bapiError') or re.search( - 'failed', response.get('bapiError'), flags=re.IGNORECASE - ): + if response.get('bapiError') or response.get("status") == "FAILURE": msg = 'Assigning to site execution with id {0} has not completed - Reason: {1}'.format( execution_id, response.get("bapiError")) self.module.fail_json(msg=msg) From 7da9a36f2dbe7a52c9fe86f401be50bb552be694 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Thu, 16 May 2024 13:29:31 +0530 Subject: [PATCH 12/78] Addressed the review comments --- .../network_settings_workflow_manager.yml | 4 +- .../network_settings_workflow_manager.py | 112 +++++++++--------- 2 files changed, 60 insertions(+), 56 deletions(-) diff --git a/playbooks/network_settings_workflow_manager.yml b/playbooks/network_settings_workflow_manager.yml index 877b7849ca..b60cc7c30d 100644 --- a/playbooks/network_settings_workflow_manager.yml +++ b/playbooks/network_settings_workflow_manager.yml @@ -62,13 +62,13 @@ settings: network_aaa: #works only if we system settigns is set ip_address: 10.0.0.21 #Mandatory for ISE, sec ip for AAA - network: 10.0.0.20 + network_address: 10.0.0.20 protocol: TACACS servers: AAA # shared_secret: string #ISE client_and_endpoint_aaa: #works only if we system settigns is set ip_address: 10.197.156.42 #Mandatory for ISE, sec ip for AAA - network: 10.0.0.20 + network_address: 10.0.0.20 protocol: RADIUS servers: AAA # shared_secret: string #ISE diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index 2d7be3c585..f213b2f197 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -241,13 +241,13 @@ ip_address: description: - Primary IP address for the ISE server. - - Secondary IP address for the AAA server. + - Primary IP address for the AAA server. - For example, 1.1.1.11. type: str - network: + network_address: description: - PAN IP address for the ISE server. - - Primary IP address for the AAA server. + - Secondary IP address for the AAA server. - For example, 1.1.1.10. type: str protocol: @@ -274,13 +274,13 @@ ip_address: description: - Primary IP address for the ISE server. - - Secondary IP address for the AAA server. + - Primary IP address for the AAA server. - For example, 1.1.1.1. type: str - network: + network_address: description: - PAN IP address for the ISE server. - - Primary IP address for the AAA server. + - Secondary IP address for the AAA server. - For example, 1.1.1.2. type: str protocol: @@ -506,11 +506,13 @@ settings: network_aaa: servers: string - network: string + ip_address: string + network_address: string protocol: string client_and_endpoint_aaa: servers: string - network: string + ip_address: string + network_address: string protocol: string dhcp_server: list dns_server: @@ -700,7 +702,7 @@ def validate_input(self): "type": 'dict', "servers": {"type": 'string', "choices": ["ISE", "AAA"]}, "ip_address": {"type": 'string'}, - "network": {"type": 'string'}, + "network_address": {"type": 'string'}, "protocol": {"type": 'string', "choices": ["RADIUS", "TACACS"]}, "shared_secret": {"type": 'string'} @@ -709,7 +711,7 @@ def validate_input(self): "type": 'dict', "servers": {"type": 'string', "choices": ["ISE", "AAA"]}, "ip_address": {"type": 'string'}, - "network": {"type": 'string'}, + "network_address": {"type": 'string'}, "protocol": {"type": 'string', "choices": ["RADIUS", "TACACS"]}, "shared_secret": {"type": 'string'} } @@ -1801,25 +1803,41 @@ def get_want_network(self, network_management_details): protocol_types = ["RADIUS", "TACACS"] network_aaa = network_management_details.get("network_aaa") if network_aaa: - if network_aaa.get("ip_address"): + servers = network_aaa.get("servers") + if servers: want_network_settings.get("network_aaa").update({ - "ipAddress": - network_aaa.get("ip_address") + "servers": servers }) else: - if network_aaa.get("servers") == "ISE": - self.msg = "missing parameter ip_address in network_aaa, server ISE is set" - self.status = "failed" - return self + want_network_settings.get("network_aaa").update({ + "servers": "ISE" + }) + + if servers not in server_types: + self.msg = "The 'servers' in the network_aaa should be in {0}".format(server_types) + self.status = "failed" + return self - if network_aaa.get("network"): + ip_address = network_aaa.get("ip_address") + if ip_address: want_network_settings.get("network_aaa").update({ - "network": network_aaa.get("network") + "ipAddress": ip_address }) else: - self.msg = "missing parameter network in network_aaa" + self.msg = "Missing required parameter 'ip_address' which is the 'primary address' in network_aaa." self.status = "failed" return self + network_address = network_aaa.get("network_address") + if network_address: + want_network_settings.get("network_aaa").update({ + "network": network_address + }) + else: + if servers == "ISE": + self.msg = "Missing required parameter 'network_address' for ISE " + \ + "which is 'PAN address' in network_aaa." + self.status = "failed" + return self protocol = network_aaa.get("protocol") if protocol: @@ -1836,25 +1854,10 @@ def get_want_network(self, network_management_details): self.status = "failed" return self - servers = network_aaa.get("servers") - if servers: - want_network_settings.get("network_aaa").update({ - "servers": servers - }) - else: - want_network_settings.get("network_aaa").update({ - "servers": "ISE" - }) - - if servers not in server_types: - self.msg = "The 'servers' in the network_aaa should be in {0}".format(server_types) - self.status = "failed" - return self - - if network_aaa.get("shared_secret"): + shared_secret = network_aaa.get("shared_secret") + if shared_secret: want_network_settings.get("network_aaa").update({ - "sharedSecret": - network_aaa.get("shared_secret") + "sharedSecret": shared_secret }) else: del want_network_settings["network_aaa"] @@ -1876,26 +1879,27 @@ def get_want_network(self, network_management_details): self.status = "failed" return self - if clientAndEndpoint_aaa.get("ip_address"): + ip_address = clientAndEndpoint_aaa.get("ip_address") + if ip_address: want_network_settings.get("clientAndEndpoint_aaa").update({ - "ipAddress": - clientAndEndpoint_aaa.get("ip_address") + "ipAddress": ip_address }) else: - if servers == "ISE": - self.msg = "Failed to process client_and_endpoint_aaa due to missing 'ip_address' parameter. ISE server is configured." - self.status = "failed" - return self + self.msg = "Missing required parameter 'ip_address' which is the 'primary address' in client_and_endpoint_aaa." + self.status = "failed" + return self - if clientAndEndpoint_aaa.get("network"): + network_address = clientAndEndpoint_aaa.get("network_address") + if network_address: want_network_settings.get("clientAndEndpoint_aaa").update({ - "network": - clientAndEndpoint_aaa.get("network") + "network": network_address }) else: - self.msg = "Failed to process client_and_endpoint_aaa due to missing parameter 'network' in the playbook." - self.status = "failed" - return self + if servers == "ISE": + self.msg = "Missing required parameter 'network_address' for ISE " + \ + "which is 'PAN address' in client_and_endpoint_aaa." + self.status = "failed" + return self protocol = clientAndEndpoint_aaa.get("protocol") if protocol: @@ -1912,10 +1916,10 @@ def get_want_network(self, network_management_details): self.status = "failed" return self - if clientAndEndpoint_aaa.get("shared_secret"): + shared_secret = clientAndEndpoint_aaa.get("shared_secret") + if shared_secret: want_network_settings.get("clientAndEndpoint_aaa").update({ - "sharedSecret": - clientAndEndpoint_aaa.get("shared_secret") + "sharedSecret": shared_secret }) else: del want_network_settings["clientAndEndpoint_aaa"] From 7ae85216d8abbc0d4dfd480ca402af3efc7eafa8 Mon Sep 17 00:00:00 2001 From: Abinash Date: Fri, 17 May 2024 03:34:40 +0000 Subject: [PATCH 13/78] PR for assigning device to site --- plugins/modules/provision_workflow_manager.py | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/plugins/modules/provision_workflow_manager.py b/plugins/modules/provision_workflow_manager.py index 981fbbb0cb..6e97807de9 100644 --- a/plugins/modules/provision_workflow_manager.py +++ b/plugins/modules/provision_workflow_manager.py @@ -317,8 +317,7 @@ def get_serial_number(self): - 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: - - serial_number: A string indicating the serial number of the device + The method returns the serial number of the device as a string. If it fails, it returns None. Example: After creating the validated input, this method retrieves the serial number of the device. @@ -359,9 +358,10 @@ def get_task_status(self, task_id=None): Parameters: - self: The instance of the class containing the 'config' attribute to be validated. + - task_id: Task_id of the provisioning task. Returns: - The method returns an instance of the class with updated attributes: - - result: A dict indiacting wheter the task was succesful or not + The method returns the status of the task_id used to track provisioning. + Returns True if task is not failed otheriwse returns False. Example: Post creation of the provision task, this method fetheches the task status. @@ -402,9 +402,10 @@ def get_execution_status(self, execution_id=None): Parameters: - self: The instance of the class containing the 'config' attribute to be validated. + - execution_id: execution_id of the BAPI API. Returns: - The method returns an instance of the class with updated attributes: - - result: A dict indiacting wheter the task was succesful or not + The method returns the status of the BAPI used to track site assignment. + Returns True if the status is not failed, otheriwse returns False. Example: Post creation of the provision task, this method fetheches the task status. @@ -442,12 +443,11 @@ def get_site_type(self, site_name_hierarchy=None): Parameters: - self: The instance of the class containing the 'config' attribute to be validated. + - site_name_hierarchy: Name of the site collected from the input. Returns: - The method returns an instance of the class with updated attributes: - - site_type: A string indicating the type of the - site (area/building/floor). + - site_type: A string indicating the type of the site (area/building/floor). Example: - Post creation of the validated input, we this method gets the + Post creation of the validated input, this method gets the type of the site. """ @@ -482,12 +482,12 @@ def get_site_details(self, site_name_hierarchy=None): Parameters: - self: The instance of the class containing the 'config' attribute to be validated. + - site_name_hierarchy: Name of the site collected from the input. Returns: - The method returns an instance of the class with updated attributes: - site_id: A string indicating the id of the site. - site_exits: A boolean value indicating the existance of the site. Example: - Post creation of the validated input, we this method gets the + Post creation of the validated input, this method gets the id of the site. """ @@ -525,11 +525,10 @@ def get_site_assignment(self): - 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: - boolean: True if any device is associated with the site, False if no device is associated with site Example: - Post creation of the validated input, this method telss whether devices are associated with a site. + Post creation of the validated input, this method tells whether devices are associated with a site. """ site_name_hierarchy = self.validated_config[0].get("site_name_hierarchy") From 6223a5e7cfb0e68fa3633c9d62969841263ebdf0 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 17 May 2024 12:33:33 +0530 Subject: [PATCH 14/78] Addressed the review comments --- .../network_settings_workflow_manager.yml | 10 +- .../network_settings_workflow_manager.py | 185 +++++++++++++----- 2 files changed, 138 insertions(+), 57 deletions(-) diff --git a/playbooks/network_settings_workflow_manager.yml b/playbooks/network_settings_workflow_manager.yml index b60cc7c30d..5b0fee2a75 100644 --- a/playbooks/network_settings_workflow_manager.yml +++ b/playbooks/network_settings_workflow_manager.yml @@ -61,14 +61,14 @@ site_name: Global/Chennai settings: network_aaa: #works only if we system settigns is set - ip_address: 10.0.0.21 #Mandatory for ISE, sec ip for AAA - network_address: 10.0.0.20 + primary_server_address: 10.0.0.20 #Mandatory for ISE, sec ip for AAA + secondary_server_address: 10.0.0.21 protocol: TACACS servers: AAA # shared_secret: string #ISE client_and_endpoint_aaa: #works only if we system settigns is set - ip_address: 10.197.156.42 #Mandatory for ISE, sec ip for AAA - network_address: 10.0.0.20 + primary_server_address: 10.197.156.42 #Mandatory for ISE, sec ip for AAA + secondary_server_address: 10.0.0.21 protocol: RADIUS servers: AAA # shared_secret: string #ISE @@ -83,7 +83,7 @@ timezone: GMT 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 diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index f213b2f197..dd9aeb36ed 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -238,23 +238,29 @@ choices: [AAA, ISE] default: ISE type: str - ip_address: - description: - - Primary IP address for the ISE server. - - Primary IP address for the AAA server. - - For example, 1.1.1.11. - type: str - network_address: - description: - - PAN IP address for the ISE server. - - Secondary IP address for the AAA server. - - For example, 1.1.1.10. - type: str protocol: description: Protocol for AAA or ISE server. choices: [RADIUS, TACACS] default: RADIUS type: str + pan_address: + description: + - PAN IP address for the ISE server. + - For example, 1.1.1.1. + type: str + version_added: 6.15.0 + primary_server_address: + description: + - Primary IP address for the ISE/AAA server. + - For example, 1.1.1.2. + type: str + version_added: 6.15.0 + secondary_server_address: + description: + - Secondary IP address for the AAA server. + - For example, 1.1.1.3. + type: str + version_added: 6.15.0 shared_secret: description: - Shared secret for ISE Server. @@ -271,23 +277,29 @@ choices: [AAA, ISE] default: ISE type: str - ip_address: + protocol: + description: Protocol for AAA or ISE server. + choices: [RADIUS, TACACS] + default: RADIUS + type: str + pan_address: description: - - Primary IP address for the ISE server. - - Primary IP address for the AAA server. + - PAN IP address for the ISE server. - For example, 1.1.1.1. type: str - network_address: + version_added: 6.15.0 + primary_server_address: description: - - PAN IP address for the ISE server. - - Secondary IP address for the AAA server. + - Primary IP address for the ISE/AAA server. - For example, 1.1.1.2. type: str - protocol: - description: Protocol for AAA or ISE server. - choices: [RADIUS, TACACS] - default: RADIUS + version_added: 6.15.0 + secondary_server_address: + description: + - Secondary IP address for the AAA server. + - For example, 1.1.1.3. type: str + version_added: 6.15.0 shared_secret: description: - Shared secret for ISE Server. @@ -533,6 +545,62 @@ syslog_server: configure_dnac_ip: True ip_addresses: list + +- name: Adding the network_aaa and client_and_endpoint_aaa AAA server + cisco.dnac.network_settings_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + dnac_log_level: "{{ dnac_log_level }}" + state: merged + config_verify: True + config: + - network_management_details: + site_name: string + settings: + network_aaa: + servers: AAA + primary_server_address: string + secondary_server_address: string + protocol: string + client_and_endpoint_aaa: + servers: AAA + primary_server_address: string + secondary_server_address: string + protocol: string + +- name: Adding the network_aaa and client_and_endpoint_aaa ISE server + cisco.dnac.network_settings_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + dnac_log_level: "{{ dnac_log_level }}" + state: merged + config_verify: True + config: + - network_management_details: + site_name: string + settings: + network_aaa: + servers: ISE + pan_address: string + primary_server_address: string + protocol: string + client_and_endpoint_aaa: + servers: ISE + pan_address: string + primary_server_address: string + protocol: string """ RETURN = r""" @@ -701,8 +769,9 @@ def validate_input(self): "network_aaa": { "type": 'dict', "servers": {"type": 'string', "choices": ["ISE", "AAA"]}, - "ip_address": {"type": 'string'}, - "network_address": {"type": 'string'}, + "pan_address": {"type": 'string'}, + "primary_server_address": {"type": 'string'}, + "secondary_server_address": {"type": 'string'}, "protocol": {"type": 'string', "choices": ["RADIUS", "TACACS"]}, "shared_secret": {"type": 'string'} @@ -710,8 +779,9 @@ def validate_input(self): "client_and_endpoint_aaa": { "type": 'dict', "servers": {"type": 'string', "choices": ["ISE", "AAA"]}, - "ip_address": {"type": 'string'}, - "network_address": {"type": 'string'}, + "pan_address": {"type": 'string'}, + "primary_server_address": {"type": 'string'}, + "secondary_server_address": {"type": 'string'}, "protocol": {"type": 'string', "choices": ["RADIUS", "TACACS"]}, "shared_secret": {"type": 'string'} } @@ -1818,26 +1888,32 @@ def get_want_network(self, network_management_details): self.status = "failed" return self - ip_address = network_aaa.get("ip_address") - if ip_address: + primary_server_address = network_aaa.get("primary_server_address") + if primary_server_address: want_network_settings.get("network_aaa").update({ - "ipAddress": ip_address + "network": primary_server_address }) else: - self.msg = "Missing required parameter 'ip_address' which is the 'primary address' in network_aaa." + self.msg = "Missing required parameter 'primary_server_address' in network_aaa." self.status = "failed" return self - network_address = network_aaa.get("network_address") - if network_address: - want_network_settings.get("network_aaa").update({ - "network": network_address - }) - else: - if servers == "ISE": - self.msg = "Missing required parameter 'network_address' for ISE " + \ - "which is 'PAN address' in network_aaa." + + if servers == "ISE": + pan_address = network_aaa.get("pan_address") + if pan_address: + want_network_settings.get("network_aaa").update({ + "ipAddress": pan_address + }) + else: + self.msg = "Missing required parameter 'pan_address' for ISE server in network_aaa." self.status = "failed" return self + else: + secondary_server_address = network_aaa.get("secondary_server_address") + if secondary_server_address: + want_network_settings.get("network_aaa").update({ + "ipAddress": secondary_server_address + }) protocol = network_aaa.get("protocol") if protocol: @@ -1879,27 +1955,32 @@ def get_want_network(self, network_management_details): self.status = "failed" return self - ip_address = clientAndEndpoint_aaa.get("ip_address") - if ip_address: + primary_server_address = clientAndEndpoint_aaa.get("primary_server_address") + if primary_server_address: want_network_settings.get("clientAndEndpoint_aaa").update({ - "ipAddress": ip_address + "network": primary_server_address }) else: - self.msg = "Missing required parameter 'ip_address' which is the 'primary address' in client_and_endpoint_aaa." + self.msg = "Missing required parameter 'primary_server_address' in client_and_endpoint_aaa." self.status = "failed" return self - network_address = clientAndEndpoint_aaa.get("network_address") - if network_address: - want_network_settings.get("clientAndEndpoint_aaa").update({ - "network": network_address - }) - else: - if servers == "ISE": - self.msg = "Missing required parameter 'network_address' for ISE " + \ - "which is 'PAN address' in client_and_endpoint_aaa." + if servers == "ISE": + pan_address = clientAndEndpoint_aaa.get("pan_address") + if pan_address: + want_network_settings.get("clientAndEndpoint_aaa").update({ + "ipAddress": pan_address + }) + else: + self.msg = "Missing required parameter 'pan_address' for ISE server in client_and_endpoint_aaa." self.status = "failed" return self + else: + secondary_server_address = clientAndEndpoint_aaa.get("secondary_server_address") + if secondary_server_address: + want_network_settings.get("clientAndEndpoint_aaa").update({ + "ipAddress": secondary_server_address + }) protocol = clientAndEndpoint_aaa.get("protocol") if protocol: From d4cc06f770e26ed31416f6df86de283d6e231b39 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 17 May 2024 15:30:31 +0530 Subject: [PATCH 15/78] Fix the issue of validating primary smtp server address in email configuration, fix the issue of syslog destination not working in one dnac version, also handle the invalid snmp privacy type --- ...ents_and_notifications_workflow_manager.py | 88 ++++++++++--------- 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/plugins/modules/events_and_notifications_workflow_manager.py b/plugins/modules/events_and_notifications_workflow_manager.py index 332c814fcb..5d2e4a9f95 100644 --- a/plugins/modules/events_and_notifications_workflow_manager.py +++ b/plugins/modules/events_and_notifications_workflow_manager.py @@ -813,43 +813,6 @@ def get_syslog_destination_in_ccc(self): self.log(self.msg, "ERROR") self.check_return_status() - def get_syslog_destination_with_name(self, name): - """ - Retrieve the details of a syslog destination with a specific name from Cisco Catalyst Center. - Args: - self (object): An instance of a class used for interacting with Cisco Catalyst Center. - name (str): The name of the syslog destination to retrieve details for. - Returns: - dict: A dictionary containing the details of the syslog destination with the specified name. - Description: - This function queries Cisco Catalyst Center to retrieve the details of a syslog destination with a specific name. - The response contains the status message indicating the syslog destination details. - If no syslog destination is found with the specified name, it returns None. - In case of any errors during the API call, an exception is raised with an error message. - """ - try: - response = self.dnac._exec( - family="event_management", - function='get_syslog_destination', - op_modifies=True, - params={"name": name} - ) - self.log("Received API response from 'get_syslog_destination': {0}".format(str(response)), "DEBUG") - response = response.get('statusMessage') - - if not response: - self.log("There is no Syslog destination added with the name '{0}' in Cisco Catalyst Center".format(name), "INFO") - return response - syslog_details = response[0] - - return syslog_details - - except Exception as e: - self.status = "failed" - self.msg = "Error while getting the details of Syslog destination with the name '{0}' from Cisco Catalyst Center: {1}".format(name, str(e)) - self.log(self.msg, "ERROR") - self.check_return_status() - def syslog_dest_needs_update(self, syslog_details, syslog_details_in_ccc): """ Check if the syslog destination needs an update based on a comparison between desired and current details. @@ -869,10 +832,15 @@ def syslog_dest_needs_update(self, syslog_details, syslog_details_in_ccc): update_needed = False for key, value in syslog_details.items(): - if str(syslog_details_in_ccc[key]) == value or value == "": + if key == "server_address": + if syslog_details_in_ccc["host"] != value: + update_needed = True + break + elif str(syslog_details_in_ccc[key]) == value or value == "": continue else: update_needed = True + break return update_needed @@ -1658,6 +1626,7 @@ def add_email_destination(self, email_params): params=email_params ) self.log("Received API response from 'create_email_destination': {0}".format(str(response)), "DEBUG") + time.sleep(2) status = response.get('statusUri') status_execution_id = status.split("/")[-1] @@ -2253,6 +2222,18 @@ def get_diff_merged(self, config): if config.get('email_destination'): email_details = self.want.get('email_details') email_params = self.collect_email_playbook_params(email_details) + primary_config = email_params.get("primarySMTPConfig") + + if primary_config and primary_config.get("hostName"): + server_address = primary_config.get("hostName") + special_chars = r'[!@#$%^&*()_+\-=\[\]{};\'\\:"|,.<>\/?]' + + if re.search(special_chars, server_address): + self.status = "failed" + self.msg = """Invalid Primary SMTP server hostname '{0}' as special character present in the input server address so + unable to add/update the email destination in Cisco Catalyst Center.""".format(server_address) + self.log(self.msg, "ERROR") + return self if not self.have.get('email_destination'): # Need to Add snmp destination in Cisco Catalyst Center with given playbook params @@ -2313,9 +2294,17 @@ def get_diff_merged(self, config): self.log(self.msg, "ERROR") return self - syslog_details_in_ccc = self.get_syslog_destination_with_name(name) + destinations_in_ccc = self.have.get('syslog_destinations') + is_destination_exist_in_ccc = False - if not syslog_details_in_ccc: + if destinations_in_ccc: + for destination in destinations_in_ccc: + if destination["name"] == name: + is_destination_exist_in_ccc = True + syslog_details_in_ccc = destination + break + + if not is_destination_exist_in_ccc: # We need to Add the Syslog Destination in the Catalyst Center self.add_syslog_destination(syslog_details).check_return_status() else: @@ -2362,6 +2351,14 @@ def get_diff_merged(self, config): self.msg = "Invalid Notification trap port '{0}' given in playbook. Select port from the number range(1, 65535)".format(port) self.log(self.msg, "ERROR") return self + privacy_type = snmp_params.get("snmpPrivacyType") + + if privacy_type and privacy_type not in ["AES128", "DES"]: + self.status = "failed" + self.msg = """Invalid SNMP Privacy type '{0}' given in playbook. Select either AES128/DES as privacy type to add/update the snmp + destination '{1}' in the Cisco Catalyst Center.""".format(privacy_type, destination_name) + self.log(self.msg, "ERROR") + return self if not is_destination_exist: # Need to Add snmp destination in Cisco Catalyst Center with given playbook params @@ -2552,9 +2549,16 @@ def verify_diff_merged(self, config): if config.get('syslog_destination'): syslog_details = self.want.get('syslog_details') syslog_name = syslog_details.get('name') - syslog_details_in_ccc = self.get_syslog_destination_with_name(syslog_name) + destinations_in_ccc = self.have.get('syslog_destinations') + is_destination_exist_in_ccc = False + + if destinations_in_ccc: + for destination in destinations_in_ccc: + if destination["name"] == syslog_name: + is_destination_exist_in_ccc = True + break - if syslog_details_in_ccc: + if is_destination_exist_in_ccc: self.status = "success" msg = """Requested Syslog Destination '{0}' have been successfully added/updated to the Cisco Catalyst Center and their addition/updation has been verified.""".format(syslog_name) From 870fc8c2a797d52d18f61d9fefe374247fcee61a Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 17 May 2024 17:40:31 +0530 Subject: [PATCH 16/78] Fix the issue of adding/updating headers while configuring webhook destination --- .../events_and_notifications_workflow_manager.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/modules/events_and_notifications_workflow_manager.py b/plugins/modules/events_and_notifications_workflow_manager.py index c753aae6ca..fbfbdd0376 100644 --- a/plugins/modules/events_and_notifications_workflow_manager.py +++ b/plugins/modules/events_and_notifications_workflow_manager.py @@ -1366,7 +1366,9 @@ def collect_webhook_playbook_params(self, webhook_details): if webhook_details.get('headers'): custom_header = webhook_details['headers'] - playbook_params['customHeaders'] = custom_header + playbook_params['headers'] = [] + for header in custom_header: + playbook_params['headers'].append(header) return playbook_params @@ -1442,11 +1444,17 @@ def webhook_dest_needs_update(self, webhook_params, webhook_dest_detail_in_ccc): """ update_needed = False + for key, value in webhook_params.items(): - if webhook_dest_detail_in_ccc[key] == value or value is None: + if isinstance(value, list): + update_needed = self.webhook_dest_needs_update(value[0], webhook_dest_detail_in_ccc[key][0]) + if update_needed: + break + elif webhook_dest_detail_in_ccc[key] == value or value is None: continue else: update_needed = True + break return update_needed From f2b36f474849da8f4adc43798dac334f02d5463f Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 17 May 2024 23:39:42 +0530 Subject: [PATCH 17/78] Addressed the review comments --- .../network_settings_workflow_manager.yml | 8 +-- .../network_settings_workflow_manager.py | 66 ++++++++----------- 2 files changed, 32 insertions(+), 42 deletions(-) diff --git a/playbooks/network_settings_workflow_manager.yml b/playbooks/network_settings_workflow_manager.yml index 5b0fee2a75..11ddde205a 100644 --- a/playbooks/network_settings_workflow_manager.yml +++ b/playbooks/network_settings_workflow_manager.yml @@ -61,16 +61,16 @@ site_name: Global/Chennai settings: network_aaa: #works only if we system settigns is set - primary_server_address: 10.0.0.20 #Mandatory for ISE, sec ip for AAA + primary_server_address: 10.0.0.20 #Mandatory for AAA and ISE secondary_server_address: 10.0.0.21 protocol: TACACS - servers: AAA + server_type: AAA # shared_secret: string #ISE client_and_endpoint_aaa: #works only if we system settigns is set - primary_server_address: 10.197.156.42 #Mandatory for ISE, sec ip for AAA + primary_server_address: 10.197.156.42 #Mandatory for AAA and ISE secondary_server_address: 10.0.0.21 protocol: RADIUS - servers: AAA + server_type: AAA # shared_secret: string #ISE dhcp_server: - 10.0.0.1 diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index dd9aeb36ed..034a59808f 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -233,7 +233,7 @@ network_aaa: description: Manages AAA (Authentication Authorization Accounting) for network devices. suboptions: - servers: + server_type: description: Server type for managing AAA for network devices. choices: [AAA, ISE] default: ISE @@ -248,30 +248,30 @@ - PAN IP address for the ISE server. - For example, 1.1.1.1. type: str - version_added: 6.15.0 + version_added: 6.14.0 primary_server_address: description: - Primary IP address for the ISE/AAA server. - For example, 1.1.1.2. type: str - version_added: 6.15.0 + version_added: 6.14.0 secondary_server_address: description: - Secondary IP address for the AAA server. - For example, 1.1.1.3. type: str - version_added: 6.15.0 + version_added: 6.14.0 shared_secret: description: - Shared secret for ISE Server. - - Required when the servers is set to ISE. + - Required when the server_type is set to ISE. - Length of the shared secret should be atleast 4 characters. type: str type: dict client_and_endpoint_aaa: description: Manages AAA (Authentication Authorization Accounting) for clients and endpoints. suboptions: - servers: + server_type: description: - Server type for managing AAA for client and endpoints. choices: [AAA, ISE] @@ -287,23 +287,23 @@ - PAN IP address for the ISE server. - For example, 1.1.1.1. type: str - version_added: 6.15.0 + version_added: 6.14.0 primary_server_address: description: - Primary IP address for the ISE/AAA server. - For example, 1.1.1.2. type: str - version_added: 6.15.0 + version_added: 6.14.0 secondary_server_address: description: - Secondary IP address for the AAA server. - For example, 1.1.1.3. type: str - version_added: 6.15.0 + version_added: 6.14.0 shared_secret: description: - Shared secret for ISE Server. - - Required when the servers is set to ISE. + - Required when the server_type is set to ISE. - Length of the shared secret should be atleast 4 characters. type: str type: dict @@ -516,16 +516,6 @@ - network_management_details: site_name: string settings: - network_aaa: - servers: string - ip_address: string - network_address: string - protocol: string - client_and_endpoint_aaa: - servers: string - ip_address: string - network_address: string - protocol: string dhcp_server: list dns_server: domain_name: string @@ -564,12 +554,12 @@ site_name: string settings: network_aaa: - servers: AAA + server_type: AAA primary_server_address: string secondary_server_address: string protocol: string client_and_endpoint_aaa: - servers: AAA + server_type: AAA primary_server_address: string secondary_server_address: string protocol: string @@ -592,12 +582,12 @@ site_name: string settings: network_aaa: - servers: ISE + server_type: ISE pan_address: string primary_server_address: string protocol: string client_and_endpoint_aaa: - servers: ISE + server_type: ISE pan_address: string primary_server_address: string protocol: string @@ -768,7 +758,7 @@ def validate_input(self): }, "network_aaa": { "type": 'dict', - "servers": {"type": 'string', "choices": ["ISE", "AAA"]}, + "server_type": {"type": 'string', "choices": ["ISE", "AAA"]}, "pan_address": {"type": 'string'}, "primary_server_address": {"type": 'string'}, "secondary_server_address": {"type": 'string'}, @@ -778,7 +768,7 @@ def validate_input(self): }, "client_and_endpoint_aaa": { "type": 'dict', - "servers": {"type": 'string', "choices": ["ISE", "AAA"]}, + "server_type": {"type": 'string', "choices": ["ISE", "AAA"]}, "pan_address": {"type": 'string'}, "primary_server_address": {"type": 'string'}, "secondary_server_address": {"type": 'string'}, @@ -1873,18 +1863,18 @@ def get_want_network(self, network_management_details): protocol_types = ["RADIUS", "TACACS"] network_aaa = network_management_details.get("network_aaa") if network_aaa: - servers = network_aaa.get("servers") - if servers: + server_type = network_aaa.get("server_type") + if server_type: want_network_settings.get("network_aaa").update({ - "servers": servers + "servers": server_type }) else: want_network_settings.get("network_aaa").update({ "servers": "ISE" }) - if servers not in server_types: - self.msg = "The 'servers' in the network_aaa should be in {0}".format(server_types) + if server_type not in server_types: + self.msg = "The 'server_type' in the network_aaa should be in {0}".format(server_types) self.status = "failed" return self @@ -1898,7 +1888,7 @@ def get_want_network(self, network_management_details): self.status = "failed" return self - if servers == "ISE": + if server_type == "ISE": pan_address = network_aaa.get("pan_address") if pan_address: want_network_settings.get("network_aaa").update({ @@ -1940,18 +1930,18 @@ def get_want_network(self, network_management_details): clientAndEndpoint_aaa = network_management_details.get("client_and_endpoint_aaa") if clientAndEndpoint_aaa: - servers = clientAndEndpoint_aaa.get("servers") - if servers: + server_type = clientAndEndpoint_aaa.get("server_type") + if server_type: want_network_settings.get("clientAndEndpoint_aaa").update({ - "servers": servers + "servers": server_type }) else: want_network_settings.get("clientAndEndpoint_aaa").update({ "servers": "ISE" }) - if servers not in server_types: - self.msg = "The 'servers' in the client_and_endpoint_aaa should be in {0}".format(server_types) + if server_type not in server_types: + self.msg = "The 'server_type' in the client_and_endpoint_aaa should be in {0}".format(server_types) self.status = "failed" return self @@ -1965,7 +1955,7 @@ def get_want_network(self, network_management_details): self.status = "failed" return self - if servers == "ISE": + if server_type == "ISE": pan_address = clientAndEndpoint_aaa.get("pan_address") if pan_address: want_network_settings.get("clientAndEndpoint_aaa").update({ From b505b81eabfeacc3ae5f4855b473c87dc5a81ed9 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Sat, 18 May 2024 16:07:28 +0530 Subject: [PATCH 18/78] Resolved the conflict when reserving a global pool which shows the config is not applied to the DNAC --- .../network_settings_workflow_manager.py | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index 034a59808f..b8af2de6ca 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -850,9 +850,6 @@ def get_obj_params(self, get_object): ("name", "name"), ("type", "type"), ("ipv6AddressSpace", "ipv6AddressSpace"), - ("ipv4GlobalPool", "ipv4GlobalPool"), - ("ipv4Prefix", "ipv4Prefix"), - ("ipv4PrefixLength", "ipv4PrefixLength"), ("ipv4GateWay", "ipv4GateWay"), ("ipv4DhcpServers", "ipv4DhcpServers"), ("ipv4DnsServers", "ipv4DnsServers"), @@ -1004,7 +1001,7 @@ def get_reserve_pool_params(self, pool_info): reserve_pool.update({"ipv4GateWay": pool_info_ippools[ipv4_index].get("gateways")[0]}) else: - reserve_pool.update({"ipv4GateWay": ""}) + reserve_pool.update({"ipv4GateWay": None}) if pool_info_ippools[ipv6_index].get("gateways") != []: reserve_pool.update({ @@ -1679,8 +1676,6 @@ def get_want_reserve_pool(self, reserve_pool): if pool_values.get("type") is None: pool_values.update({"type": "Generic"}) - if pool_values.get("ipv4GateWay") is None: - pool_values.update({"ipv4GateWay": ""}) if pool_values.get("ipv4DhcpServers") is None: pool_values.update({"ipv4DhcpServers": []}) if pool_values.get("ipv4DnsServers") is None: @@ -2418,8 +2413,13 @@ def verify_diff_merged(self, config): self.log("Current State of global pool (have): {0}" .format(self.have.get("globalPool")), "DEBUG") for item in self.want.get("wantGlobal").get("settings").get("ippool"): - if self.requires_update(self.have.get("globalPool")[global_pool_index].get("details"), - item, self.global_pool_obj_params): + global_pool_details = self.have.get("globalPool")[global_pool_index].get("details") + if not global_pool_details: + self.msg = "The global pool is not created with the config: {0}".format(item) + self.status = "failed" + return self + + if self.requires_update(global_pool_details, item, self.global_pool_obj_params): self.msg = "Global Pool Config is not applied to the Cisco Catalyst Center" self.status = "failed" return self @@ -2436,8 +2436,13 @@ def verify_diff_merged(self, config): self.log("Current State for reserve pool (have): {0}" .format(self.have.get("reservePool")), "DEBUG") for item in self.want.get("wantReserve"): - if self.requires_update(self.have.get("reservePool")[reserve_pool_index].get("details"), - item, self.reserve_pool_obj_params): + reserve_pool_details = self.have.get("reservePool")[reserve_pool_index].get("details") + if not reserve_pool_details: + self.msg = "The reserve pool is not created with the config: {0}".format(item) + self.status = "failed" + return self + + if self.requires_update(reserve_pool_details, item, self.reserve_pool_obj_params): self.msg = "Reserved Pool Config is not applied to the Cisco Catalyst Center" self.status = "failed" return self From 79f2b51fef47cd2e230b9c40c337f65d99dfb3e8 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 21 May 2024 10:25:29 +0530 Subject: [PATCH 19/78] Added the failure_policy and added the choices for software_type and product_family --- playbooks/template_workflow_manager.yml | 1 - plugins/modules/template_workflow_manager.py | 157 ++++++++++++++++--- 2 files changed, 132 insertions(+), 26 deletions(-) diff --git a/playbooks/template_workflow_manager.yml b/playbooks/template_workflow_manager.yml index 3cfed07e70..be296ff2e9 100644 --- a/playbooks/template_workflow_manager.yml +++ b/playbooks/template_workflow_manager.yml @@ -29,7 +29,6 @@ version_description: "{{ item.description }}" language: "{{ item.language }}" software_type: "{{ item.type }}" - software_variant: "{{ item.variant }}" device_types: - product_family: "{{ item.family }}" export: diff --git a/plugins/modules/template_workflow_manager.py b/plugins/modules/template_workflow_manager.py index 4a123ed2b3..1d0cb87712 100644 --- a/plugins/modules/template_workflow_manager.py +++ b/plugins/modules/template_workflow_manager.py @@ -75,6 +75,19 @@ suboptions: product_family: description: Denotes the family to which the device belongs. + choices: + - Cisco Cloud Services Platform + - Cisco Interfaces and Modules + - Content Networking + - Network Management + - NFV-ThirdParty Devices + - NFVIS + - Routers + - Security and VPN + - Storage Networking + - Switches and Hubs + - Voice and Telephony + - Wireless Controller type: str product_series: description: Specifies the series classification of the device. @@ -216,6 +229,19 @@ suboptions: product_family: description: Denotes the family to which the device belongs. + choices: + - Cisco Cloud Services Platform + - Cisco Interfaces and Modules + - Content Networking + - Network Management + - NFV-ThirdParty Devices + - NFVIS + - Routers + - Security and VPN + - Storage Networking + - Switches and Hubs + - Voice and Telephony + - Wireless Controller type: str product_series: description: Specifies the series classification of the device. @@ -223,6 +249,13 @@ product_type: description: Describes the exact type of the device. type: str + failure_policy: + description: + - Define failure policy if template provisioning fails. + - failure_policy will be enabled only when the composite is set to True. + choices: + - ABORT_TARGET_ON_ERROR + type: str id: description: A unique identifier, represented as a UUID. type: str @@ -243,9 +276,16 @@ type: str software_type: description: Applicable device software type. This field is mandatory to create a new template. - type: str - software_variant: - description: Refers to a version or edition of a software application that differs from the main or standard release. + choices: + - IOS + - IOS-XE + - IOS-XR + - NX-OS + - Cisco Controller + - Wide Area Application Services + - Adaptive Security Appliance + - NFV-OS + - Others type: str software_version: description: Applicable device software version. @@ -435,6 +475,19 @@ suboptions: product_family: description: Denotes the family to which the device belongs. + choices: + - Cisco Cloud Services Platform + - Cisco Interfaces and Modules + - Content Networking + - Network Management + - NFV-ThirdParty Devices + - NFVIS + - Routers + - Security and VPN + - Storage Networking + - Switches and Hubs + - Voice and Telephony + - Wireless Controller type: str product_series: description: Specifies the series classification of the device. @@ -573,6 +626,19 @@ suboptions: product_family: description: Denotes the family to which the device belongs. + choices: + - Cisco Cloud Services Platform + - Cisco Interfaces and Modules + - Content Networking + - Network Management + - NFV-ThirdParty Devices + - NFVIS + - Routers + - Security and VPN + - Storage Networking + - Switches and Hubs + - Voice and Telephony + - Wireless Controller type: str product_series: description: Specifies the series classification of the device. @@ -580,6 +646,13 @@ product_type: description: Describes the exact type of the device. type: str + failure_policy: + description: + - Define failure policy if template provisioning fails. + - failure_policy will be enabled only when the composite is set to True. + choices: + - ABORT_TARGET_ON_ERROR + type: str id: description: A unique identifier, represented as a UUID. type: str @@ -600,9 +673,16 @@ type: str software_type: description: Applicable device software type. This field is mandatory to create a new template. - type: str - software_variant: - description: Refers to a version or edition of a software application that differs from the main or standard release. + choices: + - IOS + - IOS-XE + - IOS-XR + - NX-OS + - Cisco Controller + - Wide Area Application Services + - Adaptive Security Appliance + - NFV-OS + - Others type: str software_version: description: Applicable device software version. @@ -761,13 +841,13 @@ - product_family: string product_series: string product_type: string + failure_policy: string id: string language: string name: string project_name: string project_description: string software_type: string - software_variant: string software_version: string tags: - id: string @@ -996,13 +1076,13 @@ def validate_input(self): 'product_series': {'type': 'str'}, 'product_type': {'type': 'str'}, }, + 'failure_policy': {'type': 'str'}, 'id': {'type': 'str'}, 'language': {'type': 'str'}, 'name': {'type': 'str'}, 'project_name': {'type': 'str'}, 'project_description': {'type': 'str'}, 'software_type': {'type': 'str'}, - 'software_variant': {'type': 'str'}, 'software_version': {'type': 'str'}, 'template_content': {'type': 'str'}, 'template_params': {'type': 'list'}, @@ -1044,13 +1124,13 @@ def validate_input(self): 'product_series': {'type': 'str'}, 'product_type': {'type': 'str'}, }, + 'failure_policy': {'type': 'str'}, 'id': {'type': 'str'}, 'language': {'type': 'str'}, 'name': {'type': 'str'}, 'project_name': {'type': 'str'}, 'project_description': {'type': 'str'}, 'software_type': {'type': 'str'}, - 'software_variant': {'type': 'str'}, 'software_version': {'type': 'str'}, 'template_content': {'type': 'str'}, 'template_params': {'type': 'list'}, @@ -1155,6 +1235,15 @@ def get_device_types(self, device_types): self.status = "failed" return self.check_return_status() + product_families_list = ["Cisco Cloud Services Platform", "Cisco Interfaces and Modules", + "Content Networking", "Network Management", "NFV-ThirdParty Devices", + "NFVIS", "Routers", "Security and VPN", "Storage Networking", + "Switches and Hubs", "Voice and Telephony", "Wireless Controller"] + if product_family not in product_families_list: + self.msg = "The 'product_family should be in the following list {0}.".format(product_families_list) + self.status = "failed" + return self.check_return_status() + product_series = item.get("product_series") if product_series is not None: deviceTypes[i].update({"productSeries": product_series}) @@ -1455,7 +1544,6 @@ def get_template_params(self, params): "deviceTypes": self.get_device_types(params.get("device_types")), "id": params.get("id"), - "softwareVariant": params.get("software_variant"), "softwareVersion": params.get("software_version"), "templateContent": params.get("template_content"), "templateParams": @@ -1499,8 +1587,26 @@ def get_template_params(self, params): self.status = "failed" return self.check_return_status() + software_types_list = ["IOS", "IOS-XE", "IOS-XR", "NX-OS", + "Cisco Controller", "Wide Area Application Services", + "Adaptive Security Appliance", "NFV-OS", "Others"] + if softwareType not in software_types_list: + self.msg = "The 'software_type' should be in the following list {0}.".format(software_types_list) + self.status = "failed" + return self.check_return_status() + temp_params.update({"softwareType": softwareType}) + if temp_params.get("composite") is True: + failure_policy = params.get("failure_policy") + failure_policy_list = ["ABORT_TARGET_ON_ERROR", None] + if failure_policy not in failure_policy_list: + self.msg = "The 'failure_policy' should be in the following list {0}.".format(failure_policy) + self.status = "failed" + return self + + temp_params.update({"failurePolicy": failure_policy}) + self.log("Formatted template params details: {0}".format(temp_params), "DEBUG") copy_temp_params = copy.deepcopy(temp_params) for item in copy_temp_params: @@ -1827,12 +1933,12 @@ def requires_update(self): ("customParamsOrder", "customParamsOrder", False), ("description", "description", ""), ("deviceTypes", "deviceTypes", []), + ("failurePolicy", "failurePolicy", ""), ("id", "id", ""), ("language", "language", "VELOCITY"), ("name", "name", ""), ("projectName", "projectName", ""), ("softwareType", "softwareType", ""), - ("softwareVariant", "softwareVariant", ""), ("softwareVersion", "softwareVersion", ""), ("templateContent", "templateContent", ""), ("templateParams", "templateParams", []), @@ -2395,8 +2501,8 @@ def verify_diff_merged(self, config): self.status = "failed" return self - template_params = ["language", "name", "projectName", "softwareType", - "softwareVariant", "templateContent"] + template_params = ["language", "name", "projectName", + "softwareType", "templateContent"] have_template = self.have_template.get("template") want_template = self.want.get("template_params") for item in template_params: @@ -2407,20 +2513,21 @@ def verify_diff_merged(self, config): return self want_template_containing_template = want_template.get("containingTemplates") - for item in want_template_containing_template: - name = item.get("name") - response = get_dict_result(have_template.get("containingTemplates"), "name", name) - if response is None: - self.msg = "Configuration Template config with template_name '{0}' under ".format(name) + \ - "'containing_templates' is not available in the Cisco Catalyst Center." - self.status = "failed" - return self - for value in item: - if item.get(value) != response.get(value): - self.msg = "Configuration Template config with template_name " + \ - "{0}'s '{1}' is not applied to the Cisco Catalyst Center.".format(name, value) + if want_template_containing_template: + for item in want_template_containing_template: + name = item.get("name") + response = get_dict_result(have_template.get("containingTemplates"), "name", name) + if response is None: + self.msg = "Configuration Template config with template_name '{0}' under ".format(name) + \ + "'containing_templates' is not available in the Cisco Catalyst Center." self.status = "failed" return self + for value in item: + if item.get(value) != response.get(value): + self.msg = "Configuration Template config with template_name " + \ + "{0}'s '{1}' is not applied to the Cisco Catalyst Center.".format(name, value) + self.status = "failed" + return self self.log("Successfully validated the Template in the Catalyst Center.", "INFO") self.result['response'][0].get("configurationTemplate").get("response").update({"Validation": "Success"}) From c3ff10fa3b1429fc68c895e8be0d1711adce9a77 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 21 May 2024 15:27:36 +0530 Subject: [PATCH 20/78] Added the validation for the name in both global pool and reserve pool, made the ip_address_space as a required parameter --- .../network_settings_workflow_manager.py | 64 ++++++++++++++++++- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index b8af2de6ca..f5862b4103 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -55,7 +55,11 @@ type: list suboptions: name: - description: Specifies the name assigned to the Global IP Pool. + description: + - Specifies the name assigned to the Global IP Pool. + - Required for the operations in the Global IP Pool. + - Length should be less than or equal to 100. + - Only letters, numbers and -_./ characters are allowed. type: str pool_type: description: > @@ -105,7 +109,11 @@ to specify where the IP sub-pool will be reserved. type: str name: - description: Name of the reserve IP subpool. + description: + - Name of the reserve IP subpool. + - Required for the operations in the Reserve IP Pool. + - Length should be less than or equal to 100. + - Only letters, numbers and -_./ characters are allowed. type: str pool_type: description: Type of the reserve ip sub pool. @@ -632,6 +640,7 @@ """ import copy +import re from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( DnacBase, @@ -1325,6 +1334,23 @@ def get_have_global_pool(self, global_pool_details): self.status = "failed" return self + name_length = len(name) + if name_length > 100: + self.msg = "The length of the'name' in global_pool_details should be less or equal to 100." + self.status = "failed" + return self + + if " " in name: + self.msg = "The 'name' in global_pool_details should not contain any spaces." + self.status = "failed" + return self + + pattern = r'^[\w\-./]+$' + if not re.match(pattern, name): + self.msg = "The 'name' in global_pool_details should contain only letters, numbers and -_./ characters." + self.status = "failed" + return self + # If the Global Pool doesn't exist and a previous name is provided # Else try using the previous name global_pool.append(self.global_pool_exists(name)) @@ -1366,6 +1392,24 @@ def get_have_reserve_pool(self, reserve_pool_details): self.msg = "Missing required parameter 'name' in reserve_pool_details." self.status = "failed" return self + + name_length = len(name) + if name_length > 100: + self.msg = "The length of the 'name' in reserve_pool_details should be less or equal to 100." + self.status = "failed" + return self + + if " " in name: + self.msg = "The 'name' in reserve_pool_details should not contain any spaces." + self.status = "failed" + return self + + pattern = r'^[\w\-./]+$' + if not re.match(pattern, name): + self.msg = "The 'name' in reserve_pool_details should contain only letters, numbers and -_./ characters." + self.status = "failed" + return self + site_name = item.get("site_name") self.log("Site Name: {0}".format(site_name), "DEBUG") if site_name is None: @@ -1549,7 +1593,6 @@ def get_want_global_pool(self, global_ippool): global_pool_index = 0 for pool_details in global_ippool: pool_values = { - "IpAddressSpace": pool_details.get("ip_address_space"), "dhcpServerIps": pool_details.get("dhcp_server_ips"), "dnsServerIps": pool_details.get("dns_server_ips"), "ipPoolName": pool_details.get("name"), @@ -1557,6 +1600,21 @@ def get_want_global_pool(self, global_ippool): "gateway": pool_details.get("gateway"), "type": pool_details.get("pool_type"), } + ip_address_space = pool_details.get("ip_address_space") + if not ip_address_space: + self.msg = "Missing required parameter 'ip_address_space' under global_pool_details." + self.status = "failed" + return self + + ip_address_space_list = ["IPv4", "IPv6"] + if ip_address_space not in ip_address_space_list: + self.msg = "The 'ip_address_space' under global_pool_details should be in the list: {0}" \ + .format(ip_address_space_list) + self.status = "failed" + return self + + pool_values.update({"IpAddressSpace": ip_address_space}) + # Converting to the required format based on the existing Global Pool if not self.have.get("globalPool")[global_pool_index].get("exists"): if pool_values.get("dhcpServerIps") is None: From fc31c7215b12044e487d10a93a7c7e41e4db6f8f Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 22 May 2024 14:46:15 +0530 Subject: [PATCH 21/78] Resolved the type problems, extra indentation and documentation issues. --- ...ise_radius_integration_workflow_manager.py | 390 ++++++++++-------- 1 file changed, 211 insertions(+), 179 deletions(-) diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index b7cccfdcfb..13ad00d822 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -107,34 +107,34 @@ - Authentication port of RADIUS server. - Updation of authentication port is not possible. - Authentication port should be from 1 to 65535. - type: str - default: "1812" + type: int + default: 1812 accounting_port: description: - Accounting port of RADIUS server. - Updation of accounting port is not possible. - Accounting port should be from 1 to 65535. - type: str - default: "1813" + type: int + default: 1813 port: description: - Port of TACACS server. - Updation of port is not possible. - Port should be from 1 to 65535. - type: str - default: "49" + type: int + default: 49 retries: description: - Number of communication retries between devices and authentication and policy server. - Retries should be from 1 to 3. - type: str - default: "3" + type: int + default: 3 timeout: description: - Number of seconds before timing out between devices and authentication and policy server. - Timeout should be from 2 to 20. - type: str - default: "4" + type: int + default: 4 role: description: - Role of authentication and policy server. @@ -250,10 +250,10 @@ - authentication_policy_server: server_type: AAA server_ip_address: 10.0.0.1 - shared_secret: 12345 + shared_secret: "12345" protocol: RADIUS_TACACS encryption_scheme: KEYWRAP - encryption_key: 1234567890123456 + encryption_key: "1234567890123456" message_authenticator_code_key: asdfghjklasdfghjklas authentication_port: 1812 accounting_port: 1813 @@ -279,10 +279,10 @@ - authentication_policy_server: server_type: ISE server_ip_address: 10.0.0.2 - shared_secret: 12345 + shared_secret: "12345" protocol: RADIUS_TACACS encryption_scheme: KEYWRAP - encryption_key: 1234567890123456 + encryption_key: "1234567890123456" message_authenticator_code_key: asdfghjklasdfghjklas authentication_port: 1812 accounting_port: 1813 @@ -294,7 +294,7 @@ pxgrid_enabled: True cisco_ise_dtos: - user_name: Cisco ISE - password: 12345 + password: "12345" fqdn: abs.cisco.com ip_address: 10.0.0.2 subscriber_name: px-1234 @@ -353,7 +353,7 @@ pxgrid_enabled: True cisco_ise_dtos: - user_name: Cisco ISE - password: 12345 + password: "12345" fqdn: abs.cisco.com ip_address: 10.0.0.2 subscriber_name: px-1234 @@ -472,11 +472,11 @@ def validate_input(self): "encryption_scheme": {"type": 'string'}, "message_authenticator_code_key": {"type": 'string'}, "encryption_key": {"type": 'string'}, - "authentication_port": {"type": 'string'}, - "accounting_port": {"type": 'string'}, - "port": {"type": 'string'}, - "retries": {"type": 'string'}, - "timeout": {"type": 'string'}, + "authentication_port": {"type": 'integer'}, + "accounting_port": {"type": 'integer'}, + "port": {"type": 'integer'}, + "retries": {"type": 'integer'}, + "timeout": {"type": 'integer'}, "role": {"type": 'string'}, "pxgrid_enabled": {"type": 'bool'}, "use_dnac_cert_for_pxgrid": {"type": 'bool'}, @@ -704,7 +704,7 @@ def get_have_authentication_policy_server(self, config): ip_address = authentication_policy_server.get("server_ip_address") if ip_address is None: - self.msg = "Mandatory Parameter server_ip_address required" + self.msg = "Missing parameter 'server_ip_address' is required." self.status = "failed" return self @@ -775,15 +775,21 @@ def get_want_authentication_policy_server(self, auth_policy_server): auth_server_exists = self.have.get("authenticationPolicyServer").get("exists") shared_secret = auth_policy_server.get("shared_secret") if not (shared_secret or auth_server_exists): - self.msg = "shared_secret is mandatory parameter" + self.msg = "Missing parameter 'shared_secret' is required." self.status = "failed" return self - if not (4 <= len(shared_secret) <= 100) or shared_secret.isspace(): + shared_secret = str(shared_secret) + if not (4 <= len(shared_secret) <= 100): self.msg = "The 'shared_secret' should contain between 4 and 100 characters." self.status = "failed" return self + if " " in shared_secret: + self.msg = "The 'shared_secret' should not contain any spaces." + self.status = "failed" + return self + if "?" in shared_secret or "<" in shared_secret: self.msg = "The 'shared_secret' should not contain '?' or '<' characters." self.status = "failed" @@ -803,7 +809,7 @@ def get_want_authentication_policy_server(self, auth_policy_server): else: auth_server.update({"protocol": "RADIUS"}) - encryption_scheme = str(auth_policy_server.get("encryption_scheme")) + encryption_scheme = auth_policy_server.get("encryption_scheme") if encryption_scheme not in ["KEYWRAP", "RADSEC", None]: self.msg = "encryption_scheme should be in ['KEYWRAP', 'RADSEC']. " + \ "It should not be {0}.".format(encryption_scheme) @@ -814,13 +820,15 @@ def get_want_authentication_policy_server(self, auth_policy_server): auth_server.update({"encryptionScheme": encryption_scheme}) if encryption_scheme == "KEYWRAP": - message_key = str(auth_policy_server.get("message_authenticator_code_key")) + message_key = auth_policy_server.get("message_authenticator_code_key") if not message_key: self.msg = "The 'message_authenticator_code_key' should not be empty if the encryption_scheme is 'KEYWRAP'." self.status = "failed" return self - if len(message_key) != 20: + message_key = str(message_key) + message_key_length = len(message_key) + if message_key_length != 20: self.msg = "The 'message_authenticator_code_key' should be exactly 20 characters." self.status = "failed" return self @@ -833,199 +841,223 @@ def get_want_authentication_policy_server(self, auth_policy_server): self.status = "failed" return self - if len(encryption_key) != 16: + encryption_key = str(encryption_key) + encryption_key_length = len(encryption_key) + if encryption_key_length != 16: self.msg = "The 'encryption_key' must be 16 characters long. It may contain alphanumeric and special characters." self.status = "failed" return self auth_server.update({"encryptionKey": encryption_key}) - authentication_port = int(auth_policy_server.get("authentication_port")) - if not 1 <= int(authentication_port) <= 65535: - self.msg = "authentication_port should be from 1 to 65535." - self.status = "failed" - return self + authentication_port = auth_policy_server.get("authentication_port") + if not authentication_port: + authentication_port = 1812 - if authentication_port: - auth_server.update({"authenticationPort": authentication_port}) - else: - auth_server.update({"authenticationPort": "1812"}) + if not str(authentication_port).isdigit(): + self.msg = "The 'authentication_port' should contain only digits." + self.status = "failed" + return self - accounting_port = int(auth_policy_server.get("accounting_port")) - if not 1 <= int(accounting_port) <= 65535: - self.msg = "accounting_port should be from 1 to 65535." - self.status = "failed" - return self + if not 1 <= authentication_port <= 65535: + self.msg = "The 'authentication_port' should be from 1 to 65535." + self.status = "failed" + return self - if accounting_port: - auth_server.update({"accountingPort": accounting_port}) - else: - auth_server.update({"accountingPort": "1813"}) + auth_server.update({"authenticationPort": authentication_port}) - port = int(auth_policy_server.get("port")) - if port: - auth_server.update({"port": port}) - else: - auth_server.update({"port": "49"}) + accounting_port = auth_policy_server.get("accounting_port") + if not accounting_port: + accounting_port = 1813 - retries = str(auth_policy_server.get("retries")) - if not retries.isdigit(): - self.msg = "retries should contain only from 0-9." - self.status = "failed" - return self + if not str(accounting_port).isdigit(): + self.msg = "The 'accounting_port' should contain only digits." + self.status = "failed" + return self - if not 1 <= int(retries) <= 3: - self.msg = "retries should be from 1 to 3." - self.status = "failed" - return self + if not 1 <= accounting_port <= 65535: + self.msg = "The 'accounting_port' should be from 1 to 65535." + self.status = "failed" + return self - if retries: - auth_server.update({"retries": retries}) - else: - auth_server.update({"retries": "3"}) + auth_server.update({"accountingPort": accounting_port}) - timeout = str(auth_policy_server.get("timeout")) - if not timeout.isdigit(): - self.msg = "timeout should contain only from 0-9." - self.status = "failed" - return self + port = auth_policy_server.get("port") + if not port: + port = 49 + + if not str(port).isdigit(): + self.msg = "The 'port' should contain only digits." + self.status = "failed" + return self + + if not 1 <= port <= 65535: + self.msg = "The 'port' should be from 1 to 65535." + self.status = "failed" + return self + + auth_server.update({"port": port}) + + retries = auth_policy_server.get("retries") + if not retries: + retries = "3" + + retries = str(retries) + if not retries.isdigit(): + self.msg = "The 'retries' should contain only from 0-9." + self.status = "failed" + return self - if not 2 <= int(timeout) <= 20: - self.msg = "timeout should be from 2 to 20." + if not 1 <= int(retries) <= 3: + self.msg = "The 'retries' should be from 1 to 3." + self.status = "failed" + return self + + auth_server.update({"retries": retries}) + + timeout = auth_policy_server.get("timeout") + if not timeout: + timeout = "4" + + timeout = str(timeout) + if not timeout.isdigit(): + self.msg = "The 'timeout' should contain only from 0-9." + self.status = "failed" + return self + + if not 2 <= int(timeout) <= 20: + self.msg = "The 'timeout' should be from 2 to 20." + self.status = "failed" + return self + + auth_server.update({"timeoutSeconds": timeout}) + + role = auth_policy_server.get("role") + if role: + auth_server.update({"role": role}) + else: + auth_server.update({"role": "secondary"}) + + if auth_server.get("isIseEnabled"): + cisco_ise_dtos = auth_policy_server.get("cisco_ise_dtos") + if not cisco_ise_dtos: + self.msg = "Missing parameter 'cisco_ise_dtos' " + \ + "required when server_type is 'ISE'." self.status = "failed" return self - if timeout: - auth_server.update({"timeoutSeconds": timeout}) - else: - auth_server.update({"timeoutSeconds": "4"}) + auth_server.update({"ciscoIseDtos": []}) + position_ise_creds = 0 + for ise_credential in cisco_ise_dtos: + auth_server.get("ciscoIseDtos").append({}) + user_name = ise_credential.get("user_name") + if not user_name: + self.msg = "Missing parameter 'user_name' is required when server_type is ISE." + self.status = "failed" + return self - role = auth_policy_server.get("role") - if role: - auth_server.update({"role": role}) - else: - auth_server.update({"role": "secondary"}) + auth_server.get("ciscoIseDtos")[position_ise_creds].update({ + "userName": user_name + }) - if auth_server.get("isIseEnabled"): - cisco_ise_dtos = auth_policy_server.get("cisco_ise_dtos") - if not cisco_ise_dtos: - self.msg = "Mandatory parameter cisco_ise_dtos " + \ - "required when server_type is 'ISE'." + password = ise_credential.get("password") + if not password: + self.msg = "Missing parameter 'password' is required when server_type is ISE." self.status = "failed" return self - auth_server.update({"ciscoIseDtos": []}) - position_ise_creds = 0 - for ise_credential in cisco_ise_dtos: - auth_server.get("ciscoIseDtos").append({}) - user_name = ise_credential.get("user_name") - if not user_name: - self.msg = "Mandatory parameter user_name required for ISE." - self.status = "failed" - return self + if not 4 <= len(password) <= 127: + self.msg = "" + self.status = "failed" + return self - auth_server.get("ciscoIseDtos")[position_ise_creds].update({ - "userName": user_name - }) + auth_server.get("ciscoIseDtos")[position_ise_creds].update({ + "password": password + }) - password = ise_credential.get("password") - if not password: - self.msg = "Mandatory paramter password required for ISE." - self.status = "failed" - return self + fqdn = ise_credential.get("fqdn") + if not fqdn: + self.msg = "Missing parameter 'fqdn' is required when server_type is ISE." + self.status = "failed" + return self - if not 4 <= len(password) <= 127: - self.msg = "" - self.status = "failed" - return self + auth_server.get("ciscoIseDtos")[position_ise_creds].update({"fqdn": fqdn}) - auth_server.get("ciscoIseDtos")[position_ise_creds].update({ - "password": password - }) + ip_address = ise_credential.get("ip_address") + if not ip_address: + self.msg = "Missing parameter 'ip_address' is required when server_type is ISE." + self.status = "failed" + return self - fqdn = ise_credential.get("fqdn") - if not fqdn: - self.msg = "Mandatory parameter required for ISE." - self.status = "failed" - return self + auth_server.get("ciscoIseDtos")[position_ise_creds].update({ + "ipAddress": ip_address + }) - auth_server.get("ciscoIseDtos")[position_ise_creds].update({"fqdn": fqdn}) + subscriber_name = ise_credential.get("subscriber_name") + if not subscriber_name: + self.msg = "Missing parameter 'subscriber_name' is required when server_type is ISE." + self.status = "failed" + return self - ip_address = ise_credential.get("ip_address") - if not ip_address: - self.msg = "Mandatory parameter ip_address required for ISE." - self.status = "failed" - return self + auth_server.get("ciscoIseDtos")[position_ise_creds].update({ + "subscriberName": subscriber_name + }) + description = ise_credential.get("description") + if description: auth_server.get("ciscoIseDtos")[position_ise_creds].update({ - "ipAddress": ip_address + "description": description }) - subscriber_name = ise_credential.get("subscriber_name") - if not subscriber_name: - self.msg = "Mandatory parameter subscriber_name required for ISE." - self.status = "failed" - return self - + ssh_key = ise_credential.get("ssh_key") + if ssh_key: auth_server.get("ciscoIseDtos")[position_ise_creds].update({ - "subscriberName": subscriber_name + "sshkey": str(ssh_key) }) - description = ise_credential.get("description") - if description: - auth_server.get("ciscoIseDtos")[position_ise_creds].update({ - "description": description - }) + position_ise_creds += 1 - ssh_key = str(ise_credential.get("ssh_key")) - if ssh_key: - auth_server.get("ciscoIseDtos")[position_ise_creds].update({ - "sshkey": ssh_key - }) + pxgrid_enabled = auth_policy_server.get("pxgrid_enabled") + if pxgrid_enabled: + auth_server.update({"pxgridEnabled": pxgrid_enabled}) + else: + auth_server.update({"pxgridEnabled": True}) - position_ise_creds += 1 - - pxgrid_enabled = auth_policy_server.get("pxgrid_enabled") - if pxgrid_enabled: - auth_server.update({"pxgridEnabled": pxgrid_enabled}) - else: - auth_server.update({"pxgridEnabled": True}) - - use_dnac_cert_for_pxgrid = auth_policy_server.get("use_dnac_cert_for_pxgrid") - if use_dnac_cert_for_pxgrid: - auth_server.update({"useDnacCertForPxgrid": use_dnac_cert_for_pxgrid}) - else: - auth_server.update({"useDnacCertForPxgrid": False}) - - external_cisco_ise_ip_addr_dtos = auth_policy_server \ - .get("external_cisco_ise_ip_addr_dtos") - if external_cisco_ise_ip_addr_dtos: - auth_server.update({"externalCiscoIseIpAddrDtos": []}) - position_ise_addresses = 0 - for external_cisco_ise in external_cisco_ise_ip_addr_dtos: - external_cisco_ise_ip_addresses = external_cisco_ise \ - .get("external_cisco_ise_ip_addresses") - if external_cisco_ise_ip_addresses: - auth_server.get("externalCiscoIseIpAddrDtos").append({}) + use_dnac_cert_for_pxgrid = auth_policy_server.get("use_dnac_cert_for_pxgrid") + if use_dnac_cert_for_pxgrid: + auth_server.update({"useDnacCertForPxgrid": use_dnac_cert_for_pxgrid}) + else: + auth_server.update({"useDnacCertForPxgrid": False}) + + external_cisco_ise_ip_addr_dtos = auth_policy_server \ + .get("external_cisco_ise_ip_addr_dtos") + if external_cisco_ise_ip_addr_dtos: + auth_server.update({"externalCiscoIseIpAddrDtos": []}) + position_ise_addresses = 0 + for external_cisco_ise in external_cisco_ise_ip_addr_dtos: + external_cisco_ise_ip_addresses = external_cisco_ise \ + .get("external_cisco_ise_ip_addresses") + if external_cisco_ise_ip_addresses: + auth_server.get("externalCiscoIseIpAddrDtos").append({}) + auth_server.get("externalCiscoIseIpAddrDtos")[position_ise_addresses] \ + .update({"externalCiscoIseIpAddresses": []}) + position_ise_address = 0 + for external_ip_address in external_cisco_ise_ip_addresses: auth_server.get("externalCiscoIseIpAddrDtos")[position_ise_addresses] \ - .update({"externalCiscoIseIpAddresses": []}) - position_ise_address = 0 - for external_ip_address in external_cisco_ise_ip_addresses: - auth_server.get("externalCiscoIseIpAddrDtos")[position_ise_addresses] \ - .get("externalCiscoIseIpAddresses").append({}) - auth_server.get("externalCiscoIseIpAddrDtos")[position_ise_addresses] \ - .get("externalCiscoIseIpAddresses")[position_ise_address].update({ - "externalIpAddress": external_ip_address.get("external_ip_address") - }) - position_ise_address += 1 - ise_type = external_cisco_ise.get("ise_type") - if ise_type: + .get("externalCiscoIseIpAddresses").append({}) auth_server.get("externalCiscoIseIpAddrDtos")[position_ise_addresses] \ - .update({"type": ise_type}) - position_ise_addresses += 1 - - if auth_policy_server.get("trusted_server"): - trusted_server = True + .get("externalCiscoIseIpAddresses")[position_ise_address].update({ + "externalIpAddress": external_ip_address.get("external_ip_address") + }) + position_ise_address += 1 + ise_type = external_cisco_ise.get("ise_type") + if ise_type: + auth_server.get("externalCiscoIseIpAddrDtos")[position_ise_addresses] \ + .update({"type": ise_type}) + position_ise_addresses += 1 + + if auth_policy_server.get("trusted_server"): + trusted_server = True self.log("Authentication and Policy Server playbook details: {0}" .format(auth_server), "DEBUG") From 95b3c7b2cef41afdae09424816dfa52854293de0 Mon Sep 17 00:00:00 2001 From: Abinash Date: Wed, 22 May 2024 10:03:23 +0000 Subject: [PATCH 22/78] Adding the condition for put API of wired device --- plugins/modules/provision_workflow_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/provision_workflow_manager.py b/plugins/modules/provision_workflow_manager.py index 6e97807de9..1a640cf505 100644 --- a/plugins/modules/provision_workflow_manager.py +++ b/plugins/modules/provision_workflow_manager.py @@ -378,7 +378,7 @@ def get_task_status(self, task_id=None): ) self.log("Response collected from 'get_task_by_id' API is {0}".format(str(response)), "DEBUG") response = response.response - self.log("Task status for the task id {0} is {1}".format(str(task_id), str(response)), "INFO") + self.log("Task status for the task id {0} is {1}".format(str(task_id), str(response.get("progress"))), "INFO") if response.get('isError') or re.search( 'failed', response.get('progress'), flags=re.IGNORECASE ): @@ -387,7 +387,7 @@ def get_task_status(self, task_id=None): self.module.fail_json(msg=msg) return False - if response.get('progress') == 'TASK_PROVISION' and response.get("isError") is False: + if response.get('progress') in ["TASK_PROVISION", "TASK_MODIFY_PUT"] and response.get("isError") is False: result = True break From 56889be8d419295b24d1e1c53591a5210018cace Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Thu, 23 May 2024 11:39:23 +0530 Subject: [PATCH 23/78] Changed the state to deleted for the delete a server --- plugins/modules/ise_radius_integration_workflow_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index 13ad00d822..b017099e96 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -370,7 +370,7 @@ dnac_debug: "{{dnac_debug}}" dnac_log: True dnac_log_level: "{{ dnac_log_level }}" - state: merged + state: deleted config_verify: True config: - authentication_policy_server: From 9caa314e685010b342290997576712e3843a9740 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 24 May 2024 13:25:28 +0530 Subject: [PATCH 24/78] Resolved the problem with the pxgridEnabled which is not set to take the user input --- plugins/modules/ise_radius_integration_workflow_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index b017099e96..7ec2804854 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -1018,7 +1018,7 @@ def get_want_authentication_policy_server(self, auth_policy_server): position_ise_creds += 1 pxgrid_enabled = auth_policy_server.get("pxgrid_enabled") - if pxgrid_enabled: + if pxgrid_enabled is not None: auth_server.update({"pxgridEnabled": pxgrid_enabled}) else: auth_server.update({"pxgridEnabled": True}) From ee360ae44d4973d355005e3ae18bd085bfb46402 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Mon, 27 May 2024 15:11:45 +0530 Subject: [PATCH 25/78] Made the get_reserve_pool iterate after 25 values. --- .../network_settings_workflow_manager.py | 56 ++++++++++++------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index f5862b4103..c76f2f5e60 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -1273,28 +1273,42 @@ def reserve_pool_exists(self, name, site_name): self.status = "failed" return reserve_pool - response = self.dnac._exec( - family="network_settings", - function="get_reserve_ip_subpool", - op_modifies=True, - params={"site_id": site_id} - ) - if not isinstance(response, dict): - reserve_pool.update({"success": False}) - self.msg = "Error in getting reserve pool - Response is not a dictionary" - self.status = "exited" - return reserve_pool - - all_reserve_pool_details = response.get("response") - reserve_pool_details = get_dict_result(all_reserve_pool_details, "groupName", name) - if not reserve_pool_details: - self.log("Reserved pool {0} does not exist in the site {1}" - .format(name, site_name), "DEBUG") - return reserve_pool + value = 1 + while True: + self.log(str(value)) + response = self.dnac._exec( + family="network_settings", + function="get_reserve_ip_subpool", + op_modifies=True, + params={ + "site_id": site_id, + "offset": value + } + ) + value += 25 + if not isinstance(response, dict): + reserve_pool.update({"success": False}) + self.msg = "Error in getting reserve pool - Response is not a dictionary" + self.log(self.msg, "CRITICAL") + self.status = "exited" + return self.check_return_status() - reserve_pool.update({"exists": True}) - reserve_pool.update({"id": reserve_pool_details.get("id")}) - reserve_pool.update({"details": self.get_reserve_pool_params(reserve_pool_details)}) + all_reserve_pool_details = response.get("response") + self.log(str(all_reserve_pool_details)) + if not all_reserve_pool_details: + self.log("Reserved pool {0} does not exist in the site {1}" + .format(name, site_name), "DEBUG") + return reserve_pool + + reserve_pool_details = get_dict_result(all_reserve_pool_details, "groupName", name) + self.log(str(reserve_pool_details)) + if reserve_pool_details: + self.log("Reserve pool found with name '{0}' in the site '{1}': {2}" + .format(name, site_name, reserve_pool_details), "INFO") + reserve_pool.update({"exists": True}) + reserve_pool.update({"id": reserve_pool_details.get("id")}) + reserve_pool.update({"details": self.get_reserve_pool_params(reserve_pool_details)}) + break self.log("Reserved pool details: {0}".format(reserve_pool.get("details")), "DEBUG") self.log("Reserved pool id: {0}".format(reserve_pool.get("id")), "DEBUG") From bc99f4f1deebe9af78421deaf30fee6892fb8e9c Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Mon, 27 May 2024 16:57:33 +0530 Subject: [PATCH 26/78] Fix the issue of validating server address in Syslog, SNMP and also validating the URL while configuring webhook destination, code rejecting valid hostName in email destination --- ...ents_and_notifications_workflow_manager.py | 109 +++++++++++------- 1 file changed, 70 insertions(+), 39 deletions(-) diff --git a/plugins/modules/events_and_notifications_workflow_manager.py b/plugins/modules/events_and_notifications_workflow_manager.py index fbfbdd0376..9b803c5f90 100644 --- a/plugins/modules/events_and_notifications_workflow_manager.py +++ b/plugins/modules/events_and_notifications_workflow_manager.py @@ -868,12 +868,6 @@ def add_syslog_destination(self, syslog_details): server_address = syslog_details.get('server_address') protocol = syslog_details.get('protocol') - if not self.is_valid_ipv4(server_address): - self.status = "failed" - self.msg = "Invalid server adderess '{0}' given in the playbook for configuring syslog destination".format(server_address) - self.log(self.msg, "ERROR") - return self - if not protocol: self.status = "failed" self.msg = "Protocol is needed while configuring the syslog destionation with name '{0}' in Cisco Catalyst Center".format(name) @@ -966,13 +960,6 @@ def update_syslog_destination(self, syslog_details, syslog_details_in_ccc): self.log(self.msg, "ERROR") return self - server_address = update_syslog_params.get('host') - if not self.is_valid_ipv4(server_address): - self.status = "failed" - self.msg = "Invalid server adderess '{0}' given in the playbook for updating syslog destination".format(server_address) - self.log(self.msg, "ERROR") - return self - response = self.dnac._exec( family="event_management", function='update_syslog_destination', @@ -1061,8 +1048,9 @@ def collect_snmp_playbook_params(self, snmp_details): 'snmpVersion': snmp_details.get('snmp_version') } server_address = snmp_details.get('server_address') + pattern = re.compile(r'^[A-Za-z0-9]([A-Za-z0-9.:-]*[A-Za-z0-9])?$') - if server_address and not self.is_valid_ipv4(server_address): + if server_address and not re.match(pattern, server_address): self.status = "failed" self.msg = "Invalid server adderess '{0}' given in the playbook for configuring SNMP destination".format(server_address) self.log(self.msg, "ERROR") @@ -1637,15 +1625,7 @@ def add_email_destination(self, email_params): time.sleep(2) status = response.get('statusUri') status_execution_id = status.split("/")[-1] - - # Now we check the status of API Events for configuring Email destination - status_response = self.dnac._exec( - family="event_management", - function='get_status_api_for_events', - op_modifies=True, - params={"execution_id": status_execution_id} - ) - self.log("Received API response from 'get_status_api_for_events': {0}".format(str(status_response)), "DEBUG") + status_response = self.check_status_api_events(status_execution_id) if status_response['apiStatus'] == "SUCCESS": self.status = "success" @@ -1708,6 +1688,47 @@ def email_dest_needs_update(self, email_params, email_dest_detail_in_ccc): return update_needed + def check_status_api_events(self, status_execution_id): + """ + Checks the status of API events in Cisco Catalyst Center until completion or timeout. + Args: + status_execution_id (str): The execution ID for the event to check the status. + Returns: + dict or None: The response from the API once the status is no longer "IN_PROGRESS", + or None if the maximum timeout is reached. + Description: + This method repeatedly checks the status of an API event in Cisco Catalyst Center using the provided + execution ID. The status is checked at intervals specified by the 'dnac_task_poll_interval' parameter + until the status is no longer "IN_PROGRESS" or the maximum timeout ('dnac_api_task_timeout') is reached. + If the status becomes anything other than "IN_PROGRESS" before the timeout, the method returns the + response from the API. If the timeout is reached first, the method logs a warning and returns None. + """ + + max_timeout = self.params.get('dnac_api_task_timeout') + events_response = None + start_time = time.time() + + while True: + end_time = time.time() + if (end_time - start_time) >= max_timeout: + self.log("""Max timeout of {0} sec has reached for the execution id '{1}' for the event and unexpected + api status so moving out of the loop.""".format(max_timeout, status_execution_id), "WARNING") + break + # Now we check the status of API Events for configuring destination and notifications + response = self.dnac._exec( + family="event_management", + function='get_status_api_for_events', + op_modifies=True, + params={"execution_id": status_execution_id} + ) + self.log("Received API response from 'get_status_api_for_events': {0}".format(str(response)), "DEBUG") + if response['apiStatus'] != "IN_PROGRESS": + events_response = response + break + time.sleep(self.params.get('dnac_task_poll_interval')) + + return events_response + def update_email_destination(self, email_details, email_dest_detail_in_ccc): """ Updates an Email destination based on the provided parameters and current details. @@ -1746,15 +1767,7 @@ def update_email_destination(self, email_details, email_dest_detail_in_ccc): time.sleep(2) status = response.get('statusUri') status_execution_id = status.split("/")[-1] - - # Now we check the status of API Events for configuring Email destination - status_response = self.dnac._exec( - family="event_management", - function='get_status_api_for_events', - op_modifies=True, - params={"execution_id": status_execution_id} - ) - self.log("Received API response from 'get_status_api_for_events': {0}".format(str(status_response)), "DEBUG") + status_response = self.check_status_api_events(status_execution_id) if status_response['apiStatus'] == "SUCCESS": self.status = "success" @@ -2195,12 +2208,22 @@ def get_diff_merged(self, config): self.log(self.msg, "ERROR") return self - regex_pattern = r'https://\S+' + regex_pattern = re.compile( + r'^https://' # Ensure the URL starts with "https://" + r'(([A-Za-z0-9-]+\.)+[A-Za-z]{2,6}|' # Domain name + r'localhost|' # Localhost + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # IPv4 + r'\[?[A-Fa-f0-9:]+\]?)' # IPv6 + r'(:\d+)?' # Optional port + r'(\/[A-Za-z0-9._~:/?#[@!$&\'()*+,;=-]*)?$' + ) url = webhook_params.get('url') - if not re.match(regex_pattern, url): + # Check if the input string matches the pattern + if url and not re.match(regex_pattern, url): self.status = "failed" - self.msg = "Given url '{0}' is invalid url for Creating/Updating Webhook destination. It must starts with 'https://'".format(url) + self.msg = """Given url '{0}' is invalid url for Creating/Updating Webhook destination. It must starts with 'https://' and + follow the valid https url format.""".format(url) self.log(self.msg, "ERROR") return self @@ -2234,9 +2257,9 @@ def get_diff_merged(self, config): if primary_config and primary_config.get("hostName"): server_address = primary_config.get("hostName") - special_chars = r'[!@#$%^&*()_+\-=\[\]{};\'\\:"|,.<>\/?]' + special_chars = r'[!@#$%^&*()_+\=\[\]{};\'\\:"|,<>\/?]' - if re.search(special_chars, server_address): + if server_address and re.search(special_chars, server_address): self.status = "failed" self.msg = """Invalid Primary SMTP server hostname '{0}' as special character present in the input server address so unable to add/update the email destination in Cisco Catalyst Center.""".format(server_address) @@ -2281,6 +2304,7 @@ def get_diff_merged(self, config): syslog_details = self.want.get('syslog_details') name = syslog_details.get('name') port = syslog_details.get('port') + server_address = syslog_details.get("server_address") if not name: self.status = "failed" @@ -2302,6 +2326,13 @@ def get_diff_merged(self, config): self.log(self.msg, "ERROR") return self + pattern = re.compile(r'^[A-Za-z0-9]([A-Za-z0-9.:-]*[A-Za-z0-9])?$') + if server_address and not pattern.match(server_address): + self.status = "failed" + self.msg = "Invalid server adderess '{0}' given in the playbook for configuring syslog destination".format(server_address) + self.log(self.msg, "ERROR") + return self + destinations_in_ccc = self.have.get('syslog_destinations') is_destination_exist_in_ccc = False @@ -2329,8 +2360,8 @@ def get_diff_merged(self, config): # Create/Update snmp destination in Cisco Catalyst Center if config.get('snmp_destination'): - snmp_details = self.want.get('snmp_details') - destination_name = snmp_details.get('name') + snmp_details = self.want.get("snmp_details") + destination_name = snmp_details.get("name") if not destination_name: self.status = "failed" From e00510ae0109639454238ab292adaff12d35cd04 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Mon, 27 May 2024 17:39:47 +0530 Subject: [PATCH 27/78] Addressed the review comments --- plugins/modules/network_settings_workflow_manager.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index c76f2f5e60..9d095acdd0 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -1220,7 +1220,6 @@ def global_pool_exists(self, name): function="get_global_pool", params={"offset": value} ) - value += 25 if not isinstance(response, dict): self.msg = "Failed to retrieve the global pool details - Response is not a dictionary" self.log(self.msg, "CRITICAL") @@ -1240,6 +1239,8 @@ def global_pool_exists(self, name): global_pool["details"] = self.get_global_pool_params(global_pool_details) break + value += 25 + self.log("Formatted global pool details: {0}".format(global_pool), "DEBUG") return global_pool @@ -1285,7 +1286,6 @@ def reserve_pool_exists(self, name, site_name): "offset": value } ) - value += 25 if not isinstance(response, dict): reserve_pool.update({"success": False}) self.msg = "Error in getting reserve pool - Response is not a dictionary" @@ -1310,6 +1310,8 @@ def reserve_pool_exists(self, name, site_name): reserve_pool.update({"details": self.get_reserve_pool_params(reserve_pool_details)}) break + value += 25 + self.log("Reserved pool details: {0}".format(reserve_pool.get("details")), "DEBUG") self.log("Reserved pool id: {0}".format(reserve_pool.get("id")), "DEBUG") return reserve_pool From 443ff479c500a3032476935fbb8ab0d9ae61b893 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Mon, 27 May 2024 18:13:46 +0530 Subject: [PATCH 28/78] After adding the Cisco ISE server, checked whether the addition is successful or it is in 'FAILED' or 'INPROGRESS' --- ...ise_radius_integration_workflow_manager.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index 7ec2804854..d386814e51 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -299,6 +299,7 @@ ip_address: 10.0.0.2 subscriber_name: px-1234 description: Cisco ISE + trusted_server: True - name: Update an AAA server. cisco.dnac.ise_radius_integration_workflow_manager: @@ -1217,6 +1218,27 @@ def update_auth_policy_server(self, ipAddress): if is_ise_server: trusted_server = self.want.get("trusted_server") self.accept_cisco_ise_server_certificate(ipAddress, trusted_server) + response = self.dnac._exec( + family="system_settings", + function='get_authentication_and_policy_servers', + params={"is_ise_enabled": True} + ) + response = response.get("response") + if response is None: + self.msg = "Failed to retrieve the information from the API 'get_authentication_and_policy_servers' of {0}." \ + .format(ipAddress) + self.status = "failed" + return + + ise_server_details = get_dict_result(response, "ipAddress", ipAddress) + ise_state_list = ["FAILED", "INPROGRESS"] + state = ise_server_details.get("state") + if state in ise_state_list: + self.msg = "The Cisco ISE server '{0}' integration is not successful. The state is '{1}'" \ + .format(ipAddress, state) + self.log(str(self.msg), "ERROR") + self.status = "failed" + return self.log("Successfully created Authentication and Policy Server '{0}'." .format(ipAddress), "INFO") From a309f543de5e2ec375460c525fea257d46eb4541 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Mon, 27 May 2024 20:29:43 +0530 Subject: [PATCH 29/78] moved check_status_api_events api to dnac.py common helper file --- plugins/module_utils/dnac.py | 43 +++++++++++++++++++ ...ents_and_notifications_workflow_manager.py | 41 ------------------ 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 2ec0d2ce0a..91cc6e8f83 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -28,6 +28,7 @@ import inspect import re import socket +import time class DnacBase(): @@ -510,6 +511,48 @@ def is_valid_ipv4(self, ip_address): except socket.error: return False + def check_status_api_events(self, status_execution_id): + """ + Checks the status of API events in Cisco Catalyst Center until completion or timeout. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + status_execution_id (str): The execution ID for the event to check the status. + Returns: + dict or None: The response from the API once the status is no longer "IN_PROGRESS", + or None if the maximum timeout is reached. + Description: + This method repeatedly checks the status of an API event in Cisco Catalyst Center using the provided + execution ID. The status is checked at intervals specified by the 'dnac_task_poll_interval' parameter + until the status is no longer "IN_PROGRESS" or the maximum timeout ('dnac_api_task_timeout') is reached. + If the status becomes anything other than "IN_PROGRESS" before the timeout, the method returns the + response from the API. If the timeout is reached first, the method logs a warning and returns None. + """ + + max_timeout = self.params.get('dnac_api_task_timeout') + events_response = None + start_time = time.time() + + while True: + end_time = time.time() + if (end_time - start_time) >= max_timeout: + self.log("""Max timeout of {0} sec has reached for the execution id '{1}' for the event and unexpected + api status so moving out of the loop.""".format(max_timeout, status_execution_id), "WARNING") + break + # Now we check the status of API Events for configuring destination and notifications + response = self.dnac._exec( + family="event_management", + function='get_status_api_for_events', + op_modifies=True, + params={"execution_id": status_execution_id} + ) + self.log("Received API response from 'get_status_api_for_events': {0}".format(str(response)), "DEBUG") + if response['apiStatus'] != "IN_PROGRESS": + events_response = response + break + time.sleep(self.params.get('dnac_task_poll_interval')) + + return events_response + def is_path_exists(self, file_path): """ Check if the file path 'file_path' exists or not. diff --git a/plugins/modules/events_and_notifications_workflow_manager.py b/plugins/modules/events_and_notifications_workflow_manager.py index 9b803c5f90..0a30c05125 100644 --- a/plugins/modules/events_and_notifications_workflow_manager.py +++ b/plugins/modules/events_and_notifications_workflow_manager.py @@ -1688,47 +1688,6 @@ def email_dest_needs_update(self, email_params, email_dest_detail_in_ccc): return update_needed - def check_status_api_events(self, status_execution_id): - """ - Checks the status of API events in Cisco Catalyst Center until completion or timeout. - Args: - status_execution_id (str): The execution ID for the event to check the status. - Returns: - dict or None: The response from the API once the status is no longer "IN_PROGRESS", - or None if the maximum timeout is reached. - Description: - This method repeatedly checks the status of an API event in Cisco Catalyst Center using the provided - execution ID. The status is checked at intervals specified by the 'dnac_task_poll_interval' parameter - until the status is no longer "IN_PROGRESS" or the maximum timeout ('dnac_api_task_timeout') is reached. - If the status becomes anything other than "IN_PROGRESS" before the timeout, the method returns the - response from the API. If the timeout is reached first, the method logs a warning and returns None. - """ - - max_timeout = self.params.get('dnac_api_task_timeout') - events_response = None - start_time = time.time() - - while True: - end_time = time.time() - if (end_time - start_time) >= max_timeout: - self.log("""Max timeout of {0} sec has reached for the execution id '{1}' for the event and unexpected - api status so moving out of the loop.""".format(max_timeout, status_execution_id), "WARNING") - break - # Now we check the status of API Events for configuring destination and notifications - response = self.dnac._exec( - family="event_management", - function='get_status_api_for_events', - op_modifies=True, - params={"execution_id": status_execution_id} - ) - self.log("Received API response from 'get_status_api_for_events': {0}".format(str(response)), "DEBUG") - if response['apiStatus'] != "IN_PROGRESS": - events_response = response - break - time.sleep(self.params.get('dnac_task_poll_interval')) - - return events_response - def update_email_destination(self, email_details, email_dest_detail_in_ccc): """ Updates an Email destination based on the provided parameters and current details. From 1f74d940a8425727618ebf02ef108ce1a0041a91 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 28 May 2024 09:36:02 +0530 Subject: [PATCH 30/78] Addressed the review comments --- plugins/modules/ise_radius_integration_workflow_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index d386814e51..c8cc5efab8 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -1231,7 +1231,7 @@ def update_auth_policy_server(self, ipAddress): return ise_server_details = get_dict_result(response, "ipAddress", ipAddress) - ise_state_list = ["FAILED", "INPROGRESS"] + ise_state_list = {"FAILED", "INPROGRESS"} state = ise_server_details.get("state") if state in ise_state_list: self.msg = "The Cisco ISE server '{0}' integration is not successful. The state is '{1}'" \ From 20f12bca8ac6626d1f6ce97bc503bb3f17a0eca2 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 28 May 2024 11:18:16 +0530 Subject: [PATCH 31/78] Addressed the review comments --- plugins/modules/ise_radius_integration_workflow_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index c8cc5efab8..69f488aa3b 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -1231,9 +1231,9 @@ def update_auth_policy_server(self, ipAddress): return ise_server_details = get_dict_result(response, "ipAddress", ipAddress) - ise_state_list = {"FAILED", "INPROGRESS"} + ise_state_set = {"FAILED", "INPROGRESS"} state = ise_server_details.get("state") - if state in ise_state_list: + if state in ise_state_set: self.msg = "The Cisco ISE server '{0}' integration is not successful. The state is '{1}'" \ .format(ipAddress, state) self.log(str(self.msg), "ERROR") From 42a4b7df61f2728401b3773925e2e7a17d98dbf1 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 28 May 2024 11:32:45 +0530 Subject: [PATCH 32/78] Add common helper function to validate server address ipv4, ipv6 and hostname in dnac.py for validation. --- plugins/module_utils/dnac.py | 26 +++++++++++++++++++ ...ents_and_notifications_workflow_manager.py | 6 ++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 91cc6e8f83..4eee046045 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -29,6 +29,7 @@ import re import socket import time +import ipaddress class DnacBase(): @@ -553,6 +554,31 @@ def check_status_api_events(self, status_execution_id): return events_response + def is_valid_server_address(self, server_address): + """ + Validates the server address to check if it's a valid IPv4, IPv6 address, or a valid hostname. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + server_address (str): The server address to validate. + Returns: + bool: True if the server address is valid, otherwise False. + """ + # Check if the address is a valid IPv4 or IPv6 address + try: + ipaddress.ip_address(server_address) + return True + except ValueError: + pass + + # Define the regex for a valid hostname + hostname_regex = re.compile(r'^(?!-)[A-Za-z0-9-]{1,63}(? Date: Tue, 28 May 2024 11:41:37 +0530 Subject: [PATCH 33/78] put ipaddress in try block --- plugins/module_utils/dnac.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 4eee046045..3501c73e88 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -17,6 +17,7 @@ from abc import ABCMeta, abstractmethod try: import logging + import ipaddress except ImportError: LOGGING_IN_STANDARD = False else: @@ -29,8 +30,6 @@ import re import socket import time -import ipaddress - class DnacBase(): From 90dd7c20052f1e1d7327b3e7b821c816519b8663 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 28 May 2024 11:45:15 +0530 Subject: [PATCH 34/78] added extra line before defining the class --- plugins/module_utils/dnac.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 3501c73e88..488a432b6e 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -31,6 +31,7 @@ import socket import time + class DnacBase(): """Class contains members which can be reused for all intent modules""" From 68fee194364822f23a2fc5c44cac4eeae11f3ffc Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 28 May 2024 12:13:16 +0530 Subject: [PATCH 35/78] Addressed the review comments --- plugins/modules/ise_radius_integration_workflow_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index 69f488aa3b..39cd942032 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -1234,8 +1234,8 @@ def update_auth_policy_server(self, ipAddress): ise_state_set = {"FAILED", "INPROGRESS"} state = ise_server_details.get("state") if state in ise_state_set: - self.msg = "The Cisco ISE server '{0}' integration is not successful. The state is '{1}'" \ - .format(ipAddress, state) + self.msg = "The Cisco ISE server '{0}' integration is not successful. The state is '{1}'. ".format(ipAddress, state) + \ + "Expected states for successful integration are not in {0}.".format(ise_state_set) self.log(str(self.msg), "ERROR") self.status = "failed" return From 8d1bfd41a6748dc8aae1f098ebf3567a23086aba Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 28 May 2024 12:19:42 +0530 Subject: [PATCH 36/78] Address review comments --- .../events_and_notifications_workflow_manager.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/modules/events_and_notifications_workflow_manager.py b/plugins/modules/events_and_notifications_workflow_manager.py index 15ce409ec1..5eabd457c0 100644 --- a/plugins/modules/events_and_notifications_workflow_manager.py +++ b/plugins/modules/events_and_notifications_workflow_manager.py @@ -1051,7 +1051,7 @@ def collect_snmp_playbook_params(self, snmp_details): if server_address and not self.is_valid_server_address(server_address): self.status = "failed" - self.msg = "Invalid server adderess '{0}' given in the playbook for configuring SNMP destination".format(server_address) + self.msg = "Invalid server address '{0}' given in the playbook for configuring SNMP destination".format(server_address) self.log(self.msg, "ERROR") self.check_return_status() @@ -2167,13 +2167,13 @@ def get_diff_merged(self, config): return self regex_pattern = re.compile( - r'^https://' # Ensure the URL starts with "https://" - r'(([A-Za-z0-9-]+\.)+[A-Za-z]{2,6}|' # Domain name + r'^https://' # Ensure the URL starts with "https://" like https://webhook.cisco.com + r'(([A-Za-z0-9-]+\.)+[A-Za-z]{2,6}|' # Domain name (e.g., example.com, webhook.cisco.com) r'localhost|' # Localhost - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # IPv4 - r'\[?[A-Fa-f0-9:]+\]?)' # IPv6 - r'(:\d+)?' # Optional port - r'(\/[A-Za-z0-9._~:/?#[@!$&\'()*+,;=-]*)?$' + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # IPv4 address (e.g., 192.168.0.1) + r'\[?[A-Fa-f0-9:]+\]?)' # IPv6 address (e.g., [2001:db8::1]) + r'(:\d+)?' # Optional port (e.g., :8080) + r'(\/[A-Za-z0-9._~:/?#[@!$&\'()*+,;=-]*)?$' # Path and query (optional) ) url = webhook_params.get('url') @@ -2286,7 +2286,7 @@ def get_diff_merged(self, config): if server_address and not self.is_valid_server_address(server_address): self.status = "failed" - self.msg = "Invalid server adderess '{0}' given in the playbook for configuring syslog destination".format(server_address) + self.msg = "Invalid server address '{0}' given in the playbook for configuring syslog destination".format(server_address) self.log(self.msg, "ERROR") return self From 44bf066911aea3592a6b40ca83cf78d6ea8ce9f8 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 28 May 2024 16:07:52 +0530 Subject: [PATCH 37/78] Resolved the problem with the prev_name for Global Pool and Reserve Pool --- plugins/modules/network_settings_workflow_manager.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index 9d095acdd0..b52cb7cdb1 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -849,6 +849,7 @@ def get_obj_params(self, get_object): try: if get_object == "GlobalPool": obj_params = [ + ("ipPoolName", "ipPoolName"), ("IpAddressSpace", "IpAddressSpace"), ("dhcpServerIps", "dhcpServerIps"), ("dnsServerIps", "dnsServerIps"), @@ -1374,11 +1375,14 @@ def get_have_global_pool(self, global_pool_details): prev_name = pool_details.get("prev_name") if global_pool[global_pool_index].get("exists") is False and \ prev_name is not None: + global_pool.pop() global_pool.append(self.global_pool_exists(prev_name)) - if global_pool.get("exists") is False: + if global_pool[global_pool_index].get("exists") is False: self.msg = "Prev name {0} doesn't exist in global_pool_details".format(prev_name) self.status = "failed" return self + + global_pool[global_pool_index].update({"prev_name": name}) global_pool_index += 1 self.log("Global pool details: {0}".format(global_pool), "DEBUG") @@ -1445,6 +1449,7 @@ def get_have_reserve_pool(self, reserve_pool_details): prev_name = item.get("prev_name") if reserve_pool[reserve_pool_index].get("exists") is False and \ prev_name is not None: + reserve_pool.pop() reserve_pool.append(self.reserve_pool_exists(prev_name, site_name)) if not reserve_pool[reserve_pool_index].get("success"): return self.check_return_status() @@ -2154,7 +2159,7 @@ def update_global_pool(self, global_pool): for item in update_global_pool: name = item.get("ipPoolName") for pool_value in self.have.get("globalPool"): - if pool_value.get("exists") and pool_value.get("details").get("ipPoolName") == name: + if pool_value.get("exists") and (pool_value.get("details").get("ipPoolName") == name or pool_value.get("prev_name") == name): if not self.requires_update(pool_value.get("details"), item, self.global_pool_obj_params): self.log("Global pool '{0}' doesn't require an update".format(name), "INFO") result_global_pool.get("msg").update({name: "Global pool doesn't require an update"}) From f5bbce905d1f64f450af423e619c7de465e257f0 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 28 May 2024 19:11:32 +0530 Subject: [PATCH 38/78] Add hostname regex with more comments --- plugins/module_utils/dnac.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 488a432b6e..4fa8ce086b 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -571,7 +571,13 @@ def is_valid_server_address(self, server_address): pass # Define the regex for a valid hostname - hostname_regex = re.compile(r'^(?!-)[A-Za-z0-9-]{1,63}(? Date: Tue, 28 May 2024 17:55:55 +0000 Subject: [PATCH 39/78] Adding feature of wireless provisioning and reprovisioning --- playbooks/device_provision_workflow.yml | 16 +- plugins/modules/provision_workflow_manager.py | 206 ++++++++++++++++-- 2 files changed, 197 insertions(+), 25 deletions(-) diff --git a/playbooks/device_provision_workflow.yml b/playbooks/device_provision_workflow.yml index 0e6e631331..61c23937a8 100644 --- a/playbooks/device_provision_workflow.yml +++ b/playbooks/device_provision_workflow.yml @@ -26,7 +26,8 @@ state: merged config_verify: True config: - - site_name_hierarchy: Global/India/Bangalore/Mantri Square + - site_name_hierarchy: Global/USA/New York/NY_BLD1 + management_ip_address: 204.192.3.40 provisioning: false @@ -48,3 +49,16 @@ state: deleted config: - management_ip_address: 204.1.2.2 + + - name: Provision a wireless device to a site + cisco.dnac.provision_workflow_manager: + <<: *dnac_login + dnac_log: True + dnac_log_level: DEBUG + state: merged + config_verify: True + config: + - site_name_hierarchy: Global/USA/RTP/BLD11 + management_ip_address: 204.192.12.201 + managed_ap_locations: + - Global/USA/RTP/BLD11/BLD11_FLOOR1 diff --git a/plugins/modules/provision_workflow_manager.py b/plugins/modules/provision_workflow_manager.py index 1a640cf505..dcb7fe75f9 100644 --- a/plugins/modules/provision_workflow_manager.py +++ b/plugins/modules/provision_workflow_manager.py @@ -54,7 +54,9 @@ description: Name of site where the device needs to be added. type: str managed_ap_locations: - description: Location of the sites allocated for the APs + description: + - Location of the sites allocated for the APs. + - This is mandatory for provisioning of wireless devices. type: list elements: str dynamic_interfaces: @@ -104,6 +106,7 @@ post /dna/intent/api/v1/wireless/provision - Added 'provisioning' option in v6.14.1 + - Added provisioning and reprovisioning of wireless devices in v6.14.1 """ @@ -161,6 +164,24 @@ management_ip_address: 204.192.3.40 provisioning: False +- name: Provision a wireless device to a site + cisco.dnac.provision_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + state: merged + config_verify: True + config: + - site_name_hierarchy: Global/USA/RTP/BLD11 + management_ip_address: 204.192.12.201 + managed_ap_locations: + - Global/USA/RTP/BLD11/BLD11_FLOOR1 + """ RETURN = r""" @@ -309,6 +330,34 @@ def get_dev_type(self): self.log("The device type is {0}".format(device_type), "INFO") return device_type + def get_device_id(self): + """ + Fetches the UUID of the device added in the inventory + + Parameters: + - self: The instance of the class containing the 'config' attribute + to be validated. + Returns: + The method returns the serial number of the device as a string. If it fails, it returns None. + Example: + After creating the validated input, this method retrieves the + UUID of the device. + """ + + dev_response = self.dnac_apply['exec']( + family="devices", + function='get_network_device_by_ip', + params={"ip_address": self.validated_config[0]["management_ip_address"]}, + op_modifies=True + ) + + self.log("The device response from 'get_network_device_by_ip' API is {0}".format(str(dev_response)), "DEBUG") + dev_dict = dev_response.get("response") + device_id = dev_dict.get("id") + + self.log("Device ID of the device is {0}".format(device_id), "INFO") + return device_id + def get_serial_number(self): """ Fetches the serial number of the device @@ -395,9 +444,9 @@ def get_task_status(self, task_id=None): self.result.update(dict(provision_task=response)) return result - def get_execution_status(self, execution_id=None): + def get_execution_status_site(self, execution_id=None): """ - Fetches the status of the task once any provision API is called + Fetches the status of the BAPI once site assignment API is called Parameters: - self: The instance of the class containing the 'config' attribute @@ -436,6 +485,52 @@ def get_execution_status(self, execution_id=None): self.result.update(dict(assignment_task=response)) return result + def get_execution_status_wireless(self, execution_id=None): + """ + Fetches the status of the BAPI once site wireless provision API is called + + Parameters: + - self: The instance of the class containing the 'config' attribute + to be validated. + - execution_id: execution_id of the BAPI API. + Returns: + The method returns the status of the BAPI used to track wireless provisioning. + Returns True if the status is not failed, otheriwse returns False. + Example: + Post creation of the provision task, this method fetheches the task + status. + + """ + result = False + params = {"execution_id": execution_id} + while True: + response = self.dnac_apply['exec']( + family="task", + function="get_business_api_execution_details", + params=params, + op_modifies=True + ) + self.log("Response collected from 'get_business_api_execution_details' API is {0}".format(str(response)), "DEBUG") + self.log("Execution status for the execution id {0} is {1}".format(str(execution_id), str(response.get("status"))), "INFO") + if response.get('bapiError') or response.get("status") == "FAILURE": + if response.get("bapiError") == "Device was already provisioned , please use provision update API to reprovision the device": + msg = "Performing reprovisioning of wireless device" + result = True + self.perform_wireless_reprovision() + break + msg = 'Wireless provisioning execution with id {0} has not completed - Reason: {1}'.format( + execution_id, response.get("bapiError")) + self.module.fail_json(msg=msg) + return False + + if response.get('status') == 'SUCCESS': + result = True + break + + time.sleep(3) + self.result.update(dict(assignment_task=response)) + return result + def get_site_type(self, site_name_hierarchy=None): """ Fetches the type of site @@ -623,31 +718,38 @@ def get_wireless_params(self): "managedAPLocations": self.validated_config[0].get("managed_ap_locations"), } ] - for ap_loc in wireless_params[0]["managedAPLocations"]: + + if not wireless_params[0].get("managedAPLocations"): + msg = "Managed AP locations must be passed as a list of sites" + self.log(msg, "CRITICAL") + self.module.fail_json(msg=msg, response=[]) + + for ap_loc in self.validated_config[0].get("managed_ap_locations"): if self.get_site_type(site_name_hierarchy=ap_loc) != "floor": self.log("Managed AP Location must be a floor", "CRITICAL") self.module.fail_json(msg="Managed AP Location must be a floor", response=[]) wireless_params[0]["dynamicInterfaces"] = [] - for interface in self.validated_config[0].get("dynamic_interfaces"): - interface_dict = { - "interfaceIPAddress": interface.get("interface_ip_address"), - "interfaceNetmaskInCIDR": interface.get("interface_netmask_in_c_i_d_r"), - "interfaceGateway": interface.get("interface_gateway"), - "lagOrPortNumber": interface.get("lag_or_port_number"), - "vlanId": interface.get("vlan_id"), - "interfaceName": interface.get("interface_name") - } - wireless_params[0]["dynamicInterfaces"].append(interface_dict) + if self.validated_config[0].get("dynamic_interfaces"): + for interface in self.validated_config[0].get("dynamic_interfaces"): + interface_dict = { + "interfaceIPAddress": interface.get("interface_ip_address"), + "interfaceNetmaskInCIDR": interface.get("interface_netmask_in_c_i_d_r"), + "interfaceGateway": interface.get("interface_gateway"), + "lagOrPortNumber": interface.get("lag_or_port_number"), + "vlanId": interface.get("vlan_id"), + "interfaceName": interface.get("interface_name") + } + wireless_params[0]["dynamicInterfaces"].append(interface_dict) response = self.dnac_apply['exec']( family="devices", function='get_network_device_by_ip', - params={"management_ip_address": self.validated_config[0]["management_ip_address"]}, + params={"ip_address": self.validated_config[0]["management_ip_address"]}, op_modifies=True ) self.log("Response collected from 'get_network_device_by_ip' is:{0}".format(str(response)), "DEBUG") - wireless_params[0]["deviceName"] = response.get("response")[0].get("hostname") + wireless_params[0]["deviceName"] = response.get("response").get("hostname") self.log("Parameters collected for the provisioning of wireless device:{0}".format(wireless_params), "INFO") return wireless_params @@ -683,6 +785,47 @@ def get_want(self): self.status = "success" return self + def perform_wireless_reprovision(self): + """ + This method performs the reprovisioning of a wireless device. Since, we don't have any + APIs to get provisioned wireless devices, so we are reprovisioning based on the failure + condition of the device + Parameters: + - self: The instance of the class containing the 'config' attribute + to be validated. + Returns: + object: An instance of the class with updated results and status + based on the processing of differences. + Example: + If wireless device is already provisioned, this method calls the provision update + API and handles it accordingly + """ + + try: + headers_payload = {"__persistbapioutput": "true"} + response = self.dnac_apply['exec']( + family="wireless", + function="provision_update", + op_modifies=True, + params={"payload": self.want.get("prov_params"), + "headers": headers_payload} + ) + self.log("Wireless provisioning response collected from 'provision' API is: {0}".format(str(response)), "DEBUG") + execution_id = response.get("executionId") + provision_info = self.get_execution_status_wireless(execution_id=execution_id) + self.result["changed"] = True + self.result['msg'] = "Wireless device got re-provisioned successfully" + self.result['diff'] = self.validated_config + self.result['response'] = execution_id + self.log(self.result['msg'], "INFO") + return self + except Exception as e: + self.log("Parameters are {0}".format(self.want)) + self.msg = "Error in wireless re-provisioning due to {0}".format(e) + self.log(self.msg, "ERROR") + self.status = "failed" + return self + def get_diff_merged(self): """ Add to provision database @@ -775,7 +918,7 @@ class instance for further use. ) self.log("Assignment response collected from 'assign_devices_to_site' API is: {0}".format(response), "DEBUG") execution_id = response.get("executionId") - assignment_info = self.get_execution_status(execution_id=execution_id) + assignment_info = self.get_execution_status_site(execution_id=execution_id) self.result["changed"] = True self.result['msg'] = "Site assignment done successfully" self.result['diff'] = self.validated_config @@ -789,13 +932,28 @@ class instance for further use. return self elif device_type == "wireless": - response = self.dnac_apply['exec']( - family="wireless", - function="provision", - op_modifies=True, - params=self.want.get("prov_params"), - ) - self.log("Wireless provisioning response collected from 'provision' API is: {0}".format(response), "DEBUG") + try: + response = self.dnac_apply['exec']( + family="wireless", + function="provision", + op_modifies=True, + params={"payload": self.want.get("prov_params")} + ) + self.log("Wireless provisioning response collected from 'provision' API is: {0}".format(str(response)), "DEBUG") + execution_id = response.get("executionId") + provision_info = self.get_execution_status_wireless(execution_id=execution_id) + self.result["changed"] = True + self.result['msg'] = "Wireless device got provisioned successfully" + self.result['diff'] = self.validated_config + self.result['response'] = execution_id + self.log(self.result['msg'], "INFO") + return self + except Exception as e: + self.log("Parameters are {0}".format(self.want)) + self.msg = "Error in wireless provisioning due to {0}".format(e) + self.log(self.msg, "ERROR") + self.status = "failed" + return self else: self.result['msg'] = "Passed device is neither wired nor wireless" From 327f4e60750138a6366a9dc090cef9f4be733bb3 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Tue, 28 May 2024 18:36:11 -0700 Subject: [PATCH 40/78] CSCwk13435, CSCwk13995, CSCwk13555 bugs fixed --- .../network_compliance_workflow_manager.py | 66 ++++++++++++++----- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/plugins/modules/network_compliance_workflow_manager.py b/plugins/modules/network_compliance_workflow_manager.py index d0b716d5c1..7767853754 100644 --- a/plugins/modules/network_compliance_workflow_manager.py +++ b/plugins/modules/network_compliance_workflow_manager.py @@ -58,6 +58,7 @@ description: Determines if a full compliance check should be triggered on the devices specified in the "ip_address_list" and/or "site_name". if it is True then compliance will be triggered for all categories. If it is False then compliance will be not be triggered even if run_compliance categories are provided. + Note: This operation cannot be performed on Access Points (APs) and if APs are provided, they will be skipped. type: bool default: True run_compliance_categories: @@ -73,6 +74,7 @@ Sync device configuration, primarily addresses the status of the `RUNNING_CONFIG`. If set to True, and if `RUNNING_CONFIG` status is non-compliant this operation would commit device running configuration to startup by issuing "write memory" to device. + Note: This operation cannot be performed on Access Points (APs) and if APs are provided, they will be skipped. type: bool default: False @@ -274,7 +276,7 @@ """ RETURN = r""" -#Case_1: Response when Network Compliance operations are performed successfully on device/s. +#Case_1: Response when Run Compliance operation is performed successfully on device/s. sample_response_2: description: A dictionary with the response returned by the Cisco Catalyst Center Python SDK returned: always @@ -288,10 +290,28 @@ "taskId": "string", "url": "string" }, + "data": dict, "version": "string" } -#Case_2: Response when Error Occurs in performing Run Compliance or Sync Device Configuration operation on device/s. +#Case_2: Response when Sync Device Configuration operation is performed successfully on device/s. +sample_response_2: + description: A dictionary with the response returned by the Cisco Catalyst Center Python SDK + returned: always + type: dict + sample: > + { + "status": "string", + "changed": bool, + "msg": "string" + "response": { + "taskId": "string", + "url": "string" + }, + "version": "string" + } + +#Case_3: Response when Error Occurs in performing Run Compliance or Sync Device Configuration operation on device/s. sample_response_3: description: A dictionary with the response returned by the Cisco Catalyst Center Python SDK returned: always @@ -535,7 +555,7 @@ def get_device_ids_from_ip(self, ip_address_list): op_modifies=True, params={"managementIpAddress": device_ip} ) - self.log("Response received post 'get_device_list' API call: {0} ".format(str(response)), "DEBUG") + self.log("Response received post 'get_device_list' API call: {0}".format(str(response)), "DEBUG") # Check if a valid response is received if response.get("response"): @@ -544,8 +564,15 @@ def get_device_ids_from_ip(self, ip_address_list): continue for device_info in response: if device_info["reachabilityStatus"] == "Reachable": - device_id = response[0]["id"] - mgmt_ip_instance_id_map[device_ip] = device_id + if device_info["family"] != "Unified AP": + device_id = device_info["id"] + mgmt_ip_instance_id_map[device_ip] = device_id + else: + msg = "Skipping device {0} as its family is {1}.".format(device_ip, device_info["family"]) + self.log(msg, "INFO") + else: + msg = "Skipping device {0} as its status is {2}.".format(device_ip, device_info["reachabilityStatus"]) + self.log(msg, "INFO") else: # If unable to retrieve device information, log an error message self.log("Unable to retrieve device information for {0}. Please ensure that the device exists and is reachable.".format(device_ip), "ERROR") @@ -554,11 +581,11 @@ def get_device_ids_from_ip(self, ip_address_list): # Log an error message if any exception occurs during the process self.log("Error while fetching device ID for device: '{0}' from Cisco Catalyst Center: {1}".format(device_ip, str(e)), "ERROR") - if not mgmt_ip_instance_id_map: - msg = "Error occurred while retrieving device details (Device UUID) using the 'get_device_list' API " - msg += "for the following device(s): {0}".format(ip_address_list) - self.log(msg, "ERROR") - self.module.fail_json(msg=msg) + # if not mgmt_ip_instance_id_map: + # msg = "Error occurred while retrieving device details (Device UUID) using the 'get_device_list' API " + # msg += "for the following device(s): {0}".format(ip_address_list) + # self.log(msg, "ERROR") + # self.module.fail_json(msg=msg) return mgmt_ip_instance_id_map @@ -605,12 +632,16 @@ def get_device_ids_from_site(self, site_name, site_id): for item_dict in item["response"]: # Check if the device is reachable if item_dict["reachabilityStatus"] == "Reachable": - mgmt_ip_instance_id_map[item_dict["managementIpAddress"]] = item_dict["instanceUuid"] + if item_dict["family"] != "Unified AP": + mgmt_ip_instance_id_map[item_dict["managementIpAddress"]] = item_dict["instanceUuid"] + else: + msg = "Skipping device {0} in site {1} as its family is {2}".format( + item_dict["managementIpAddress"], site_name, item_dict["family"]) + self.log(msg, "INFO") else: - msg = "Unable to get deviceId for device {0} in site {1} as its status is {2}".format( - item["managementIpAddress"], site_name, item["reachabilityStatus"]) - self.log(msg, "CRITICAL") - self.module.fail_json(msg=msg) + msg = "Skipping device {0} in site {1} as its status is {2}".format( + item_dict["managementIpAddress"], site_name, item_dict["reachabilityStatus"]) + self.log(msg, "WARNING") else: # If unable to retrieve device information, log an error message self.log("No response received from API call to get membership information for site. {0}".format(site_name), "ERROR") @@ -761,7 +792,8 @@ def get_want(self, config): mgmt_ip_instance_id_map = self.get_device_id_list(ip_address_list, site_name) if not mgmt_ip_instance_id_map: # Log an error message if mgmt_ip_instance_id_map is empty - msg = "Failed to retrieve device IDs for the provided IP addresses: {0} or site name: {1}.".format(ip_address_list, site_name) + msg = ("No device UUIDs were fetched for network compliance operations with the provided IP addresses: {0} " + "or site name: {1}. This could be due to Unreachable devices or access points (APs).").format(ip_address_list, site_name) self.log(msg, "ERROR") self.module.fail_json(msg) @@ -1164,7 +1196,7 @@ def get_compliance_task_status(self, task_id, mgmt_ip_instance_id_map): task_name, list(mgmt_ip_instance_id_map.keys()), modified_response), "INFO") # Update result with modified response - self.update_result("success", True, self.msg, "INFO") + self.update_result("success", True, self.msg, "INFO", modified_response) break # Check if task failed From 20d21b2993785cb69fad54b1bebc05f66d3b6579 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Tue, 28 May 2024 18:54:31 -0700 Subject: [PATCH 41/78] CSCwk13435, CSCwk13995, CSCwk13555 bugs fixed --- plugins/modules/network_compliance_workflow_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/network_compliance_workflow_manager.py b/plugins/modules/network_compliance_workflow_manager.py index 7767853754..173cebb952 100644 --- a/plugins/modules/network_compliance_workflow_manager.py +++ b/plugins/modules/network_compliance_workflow_manager.py @@ -58,7 +58,7 @@ description: Determines if a full compliance check should be triggered on the devices specified in the "ip_address_list" and/or "site_name". if it is True then compliance will be triggered for all categories. If it is False then compliance will be not be triggered even if run_compliance categories are provided. - Note: This operation cannot be performed on Access Points (APs) and if APs are provided, they will be skipped. + Note - This operation cannot be performed on Access Points (APs) and if APs are provided, they will be skipped. type: bool default: True run_compliance_categories: @@ -74,7 +74,7 @@ Sync device configuration, primarily addresses the status of the `RUNNING_CONFIG`. If set to True, and if `RUNNING_CONFIG` status is non-compliant this operation would commit device running configuration to startup by issuing "write memory" to device. - Note: This operation cannot be performed on Access Points (APs) and if APs are provided, they will be skipped. + Note - This operation cannot be performed on Access Points (APs) and if APs are provided, they will be skipped. type: bool default: False @@ -277,7 +277,7 @@ RETURN = r""" #Case_1: Response when Run Compliance operation is performed successfully on device/s. -sample_response_2: +sample_response_1: description: A dictionary with the response returned by the Cisco Catalyst Center Python SDK returned: always type: dict From 17c8cd31d893fdf5c31da43d4da8685ae921958c Mon Sep 17 00:00:00 2001 From: Abinash Date: Wed, 29 May 2024 05:41:37 +0000 Subject: [PATCH 42/78] Adding feature of wireless provisioning and reprovisioning --- plugins/modules/provision_workflow_manager.py | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/plugins/modules/provision_workflow_manager.py b/plugins/modules/provision_workflow_manager.py index dcb7fe75f9..5f56989ff9 100644 --- a/plugins/modules/provision_workflow_manager.py +++ b/plugins/modules/provision_workflow_manager.py @@ -309,13 +309,16 @@ def get_dev_type(self): Post creation of the validated input, we this method gets the type of the device. """ - - dev_response = self.dnac_apply['exec']( - family="devices", - function='get_network_device_by_ip', - params={"ip_address": self.validated_config[0]["management_ip_address"]}, - op_modifies=True - ) + try: + dev_response = self.dnac_apply['exec']( + family="devices", + function='get_network_device_by_ip', + params={"ip_address": self.validated_config[0]["management_ip_address"]}, + op_modifies=True + ) + except Exception as e: + self.log(str(e), "ERROR") + self.module.fail_json(msg=str(e)) self.log("The device response from 'get_network_device_by_ip' API is {0}".format(str(dev_response)), "DEBUG") dev_dict = dev_response.get("response") @@ -719,8 +722,9 @@ def get_wireless_params(self): } ] - if not wireless_params[0].get("managedAPLocations"): - msg = "Managed AP locations must be passed as a list of sites" + if not (wireless_params[0].get("managedAPLocations") and isinstance(wireless_params[0].get("managedAPLocations"), list)): + msg = "Managed AP locations must be passed as a list of sites. For example, [Global/USA/RTP/BLD11/BLD11_FLOOR1,\ + Global/USA/RTP/BLD11/BLD11_FLOOR2]" self.log(msg, "CRITICAL") self.module.fail_json(msg=msg, response=[]) @@ -810,18 +814,18 @@ def perform_wireless_reprovision(self): params={"payload": self.want.get("prov_params"), "headers": headers_payload} ) - self.log("Wireless provisioning response collected from 'provision' API is: {0}".format(str(response)), "DEBUG") + self.log("Wireless provisioning response collected from 'provision_update' API is: {0}".format(str(response)), "DEBUG") execution_id = response.get("executionId") provision_info = self.get_execution_status_wireless(execution_id=execution_id) self.result["changed"] = True - self.result['msg'] = "Wireless device got re-provisioned successfully" + self.result['msg'] = "Wireless device with IP address {0} got re-provisioned successfully".format(self.validated_config[0]["management_ip_address"]) self.result['diff'] = self.validated_config self.result['response'] = execution_id self.log(self.result['msg'], "INFO") return self except Exception as e: self.log("Parameters are {0}".format(self.want)) - self.msg = "Error in wireless re-provisioning due to {0}".format(e) + self.msg = "Error in wireless re-provisioning of {0} due to {1}".format(self.validated_config[0]["management_ip_address"], e) self.log(self.msg, "ERROR") self.status = "failed" return self @@ -943,14 +947,14 @@ class instance for further use. execution_id = response.get("executionId") provision_info = self.get_execution_status_wireless(execution_id=execution_id) self.result["changed"] = True - self.result['msg'] = "Wireless device got provisioned successfully" + self.result['msg'] = "Wireless device with IP {0} got provisioned successfully".format(self.validated_config[0]["management_ip_address"]) self.result['diff'] = self.validated_config self.result['response'] = execution_id self.log(self.result['msg'], "INFO") return self except Exception as e: self.log("Parameters are {0}".format(self.want)) - self.msg = "Error in wireless provisioning due to {0}".format(e) + self.msg = "Error in wireless provisioning of {0}due to {1}".format(self.validated_config[0]["management_ip_address"], e) self.log(self.msg, "ERROR") self.status = "failed" return self @@ -1082,7 +1086,7 @@ def verify_diff_merged(self): self.log("Requested wired device is not provisioned", "INFO") else: - self.log("Currently we don't have any API in the Cisco Catalyst Center to fetch the provisioning details of wired devices") + self.log("Currently we don't have any API in the Cisco Catalyst Center to fetch the provisioning details of wireless devices") self.status = "success" return self @@ -1128,7 +1132,7 @@ def verify_diff_deleted(self): self.log("Requested wired device is unprovisioned", "INFO") else: - self.log("Currently we don't have any API in the Cisco Catalyst Center to fetch the provisioning details of wired devices") + self.log("Currently we don't have any API in the Cisco Catalyst Center to fetch the provisioning details of wireless devices") self.status = "success" return self From 8390a128c50dd929a64c06a49efedda790d1ee27 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 29 May 2024 13:01:43 +0530 Subject: [PATCH 43/78] Supports the ISE server 'username' and 'password' updation, made port number default to 49 --- ...se_radius_integration_workflow_manager.yml | 2 - ...ise_radius_integration_workflow_manager.py | 48 +++++-------------- 2 files changed, 11 insertions(+), 39 deletions(-) diff --git a/playbooks/ise_radius_integration_workflow_manager.yml b/playbooks/ise_radius_integration_workflow_manager.yml index 28dd61696f..22a0e5ce28 100644 --- a/playbooks/ise_radius_integration_workflow_manager.yml +++ b/playbooks/ise_radius_integration_workflow_manager.yml @@ -29,7 +29,6 @@ message_authenticator_code_key: dnacisesolutions1234 # For KEYWRAP, must be 20 char long authentication_port: 1800 accounting_port: 1700 - port: 40 # For TACACS retries: 3 # Range from 1 to 3 timeout: 4 # Range from 2 to 20 role: secondary @@ -77,7 +76,6 @@ message_authenticator_code_key: dnacisesolutions1234 # For KEYWRAP, must be 20 char long authentication_port: 1800 accounting_port: 1700 - port: 40 # For TACACS retries: 3 # Range from 1 to 3 timeout: 4 # Range from 2 to 20 role: primary diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index 39cd942032..c1a537f792 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -116,13 +116,6 @@ - Accounting port should be from 1 to 65535. type: int default: 1813 - port: - description: - - Port of TACACS server. - - Updation of port is not possible. - - Port should be from 1 to 65535. - type: int - default: 49 retries: description: - Number of communication retries between devices and authentication and policy server. @@ -257,7 +250,6 @@ message_authenticator_code_key: asdfghjklasdfghjklas authentication_port: 1812 accounting_port: 1813 - port: 49 retries: 3 timeout: 4 role: secondary @@ -286,7 +278,6 @@ message_authenticator_code_key: asdfghjklasdfghjklas authentication_port: 1812 accounting_port: 1813 - port: 49 retries: 3 timeout: 4 role: primary @@ -321,7 +312,6 @@ protocol: RADIUS_TACACS authentication_port: 1812 accounting_port: 1813 - port: 49 retries: 3 timeout: 5 role: secondary @@ -346,7 +336,6 @@ protocol: RADIUS_TACACS authentication_port: 1812 accounting_port: 1813 - port: 49 retries: 3 timeout: 5 role: primary @@ -423,6 +412,7 @@ """ import copy +import time from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( DnacBase, @@ -475,7 +465,6 @@ def validate_input(self): "encryption_key": {"type": 'string'}, "authentication_port": {"type": 'integer'}, "accounting_port": {"type": 'integer'}, - "port": {"type": 'integer'}, "retries": {"type": 'integer'}, "timeout": {"type": 'integer'}, "role": {"type": 'string'}, @@ -564,12 +553,9 @@ def get_obj_params(self, get_object): obj_params = [] if get_object == "authenticationPolicyServer": obj_params = [ - ("pxgridEnabled", "pxgridEnabled"), - ("useDnacCertForPxgrid", "useDnacCertForPxgrid"), ("protocol", "protocol"), ("retries", "retries"), - ("timeoutSeconds", "timeoutSeconds"), - ("externalCiscoIseIpAddrDtos", "externalCiscoIseIpAddrDtos") + ("timeoutSeconds", "timeoutSeconds") ] else: raise ValueError("Received an unexpected value for 'get_object': {0}" @@ -752,7 +738,7 @@ def get_want_authentication_policy_server(self, auth_policy_server): Parameters: auth_policy_server (dict) - Playbook authentication policy server details containing IpAddress, authentication port, accounting port, Cisco ISE Details, - protocol, port, retries, role, timeout seconds, encryption details. + protocol, retries, role, timeout seconds, encryption details. Returns: self - The current object with updated desired Authentication Policy Server information. @@ -810,6 +796,8 @@ def get_want_authentication_policy_server(self, auth_policy_server): else: auth_server.update({"protocol": "RADIUS"}) + auth_server.update({"port": 49}) + encryption_scheme = auth_policy_server.get("encryption_scheme") if encryption_scheme not in ["KEYWRAP", "RADSEC", None]: self.msg = "encryption_scheme should be in ['KEYWRAP', 'RADSEC']. " + \ @@ -883,22 +871,6 @@ def get_want_authentication_policy_server(self, auth_policy_server): auth_server.update({"accountingPort": accounting_port}) - port = auth_policy_server.get("port") - if not port: - port = 49 - - if not str(port).isdigit(): - self.msg = "The 'port' should contain only digits." - self.status = "failed" - return self - - if not 1 <= port <= 65535: - self.msg = "The 'port' should be from 1 to 65535." - self.status = "failed" - return self - - auth_server.update({"port": port}) - retries = auth_policy_server.get("retries") if not retries: retries = "3" @@ -1156,7 +1128,7 @@ def format_payload_for_update(self, have_auth_server, want_auth_server): if want_auth_server.get("encryptionKey") is not None: del want_auth_server["encryptionKey"] - update_params = ["authenticationPort", "accountingPort", "port", "role"] + update_params = ["authenticationPort", "accountingPort", "role"] for item in update_params: have_auth_server_item = have_auth_server.get(item) want_auth_server_item = want_auth_server.get(item) @@ -1218,6 +1190,7 @@ def update_auth_policy_server(self, ipAddress): if is_ise_server: trusted_server = self.want.get("trusted_server") self.accept_cisco_ise_server_certificate(ipAddress, trusted_server) + time.sleep(15) response = self.dnac._exec( family="system_settings", function='get_authentication_and_policy_servers', @@ -1256,9 +1229,10 @@ def update_auth_policy_server(self, ipAddress): # Edit API not working, remove this self.format_payload_for_update(self.have.get("authenticationPolicyServer").get("details"), self.want.get("authenticationPolicyServer")).check_return_status() - if not self.requires_update(self.have.get("authenticationPolicyServer").get("details"), - self.want.get("authenticationPolicyServer"), - self.authentication_policy_server_obj_params): + is_ise_server_enabled = self.have.get("authenticationPolicyServer").get("details").get("isIseEnabled") + if not (is_ise_server_enabled or self.requires_update(self.have.get("authenticationPolicyServer").get("details"), + self.want.get("authenticationPolicyServer"), + self.authentication_policy_server_obj_params)): self.log("Authentication and Policy Server '{0}' doesn't require an update" .format(ipAddress), "INFO") result_auth_server.get("response").get(ipAddress).update({ From d22fda98a701888b4016400c4afc3bf079231bc9 Mon Sep 17 00:00:00 2001 From: Abinash Date: Wed, 29 May 2024 10:07:20 +0000 Subject: [PATCH 44/78] Adding feature of wireless provisioning and reprovisioning --- plugins/modules/provision_workflow_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/provision_workflow_manager.py b/plugins/modules/provision_workflow_manager.py index 5f56989ff9..476a997fb2 100644 --- a/plugins/modules/provision_workflow_manager.py +++ b/plugins/modules/provision_workflow_manager.py @@ -358,7 +358,7 @@ def get_device_id(self): dev_dict = dev_response.get("response") device_id = dev_dict.get("id") - self.log("Device ID of the device is {0}".format(device_id), "INFO") + self.log("Device ID of the device with IP address {0} is {1}".format(self.validated_config[0]["management_ip_address"], device_id), "INFO") return device_id def get_serial_number(self): @@ -954,7 +954,7 @@ class instance for further use. return self except Exception as e: self.log("Parameters are {0}".format(self.want)) - self.msg = "Error in wireless provisioning of {0}due to {1}".format(self.validated_config[0]["management_ip_address"], e) + self.msg = "Error in wireless provisioning of {0} due to {1}".format(self.validated_config[0]["management_ip_address"], e) self.log(self.msg, "ERROR") self.status = "failed" return self From cb95c08076ecbf6db4f9c781f9ba973d2a27e2b1 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 29 May 2024 16:26:16 +0530 Subject: [PATCH 45/78] Fixed the problem with the sites in assign_device_credentials --- plugins/modules/device_credential_workflow_manager.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/modules/device_credential_workflow_manager.py b/plugins/modules/device_credential_workflow_manager.py index 48ec91efdd..fdbc6ed921 100644 --- a/plugins/modules/device_credential_workflow_manager.py +++ b/plugins/modules/device_credential_workflow_manager.py @@ -889,7 +889,12 @@ def get_site_id(self, site_name): .format(site_name), "ERROR") return None - _id = response.get("response")[0].get("id") + response = response.get("response") + if not response: + self.log("The site with the name '{0}' is not valid".format(site_name), "ERROR") + return None + + _id = response[0].get("id") self.log("Site ID for the site name {0}: {1}".format(site_name, _id), "INFO") except Exception as e: self.log("Exception occurred while getting site_id from the site_name: {0}" @@ -1961,7 +1966,7 @@ def get_want_assign_credentials(self, AssignCredentials): site_id = [] for site_name in site_name: siteId = self.get_site_id(site_name) - if not site_id: + if not siteId: self.msg = "The site_name '{0}' is invalid in 'assign_credentials_to_site'".format(site_name) self.status = "failed" return self From 3ffa1335eed73239c46d1e8fd71dea1e20f6996a Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 29 May 2024 18:21:27 +0530 Subject: [PATCH 46/78] Addressed the review comments --- .../device_credential_workflow_manager.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/modules/device_credential_workflow_manager.py b/plugins/modules/device_credential_workflow_manager.py index fdbc6ed921..6445f0671f 100644 --- a/plugins/modules/device_credential_workflow_manager.py +++ b/plugins/modules/device_credential_workflow_manager.py @@ -1957,22 +1957,22 @@ def get_want_assign_credentials(self, AssignCredentials): want = { "assign_credentials": {} } - site_name = AssignCredentials.get("site_name") - if not site_name: + site_names = AssignCredentials.get("site_name") + if not site_names: self.msg = "The 'site_name' is required parameter for 'assign_credentials_to_site'" self.status = "failed" return self - site_id = [] - for site_name in site_name: - siteId = self.get_site_id(site_name) - if not siteId: + site_ids = [] + for site_name in site_names: + current_site_id = self.get_site_id(site_name) + if not current_site_id: self.msg = "The site_name '{0}' is invalid in 'assign_credentials_to_site'".format(site_name) self.status = "failed" return self - site_id.append(siteId) + site_ids.append(current_site_id) - want.update({"site_id": site_id}) + want.update({"site_id": site_ids}) global_credentials = self.get_global_credentials_params() cli_credential = AssignCredentials.get("cli_credential") if cli_credential: From 355dbf56278f449c6d8481d8fff21d23e2c9bde1 Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Wed, 29 May 2024 09:15:21 -0700 Subject: [PATCH 47/78] addressed review comments --- .../modules/network_compliance_workflow_manager.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/plugins/modules/network_compliance_workflow_manager.py b/plugins/modules/network_compliance_workflow_manager.py index 173cebb952..cb335f5570 100644 --- a/plugins/modules/network_compliance_workflow_manager.py +++ b/plugins/modules/network_compliance_workflow_manager.py @@ -581,12 +581,6 @@ def get_device_ids_from_ip(self, ip_address_list): # Log an error message if any exception occurs during the process self.log("Error while fetching device ID for device: '{0}' from Cisco Catalyst Center: {1}".format(device_ip, str(e)), "ERROR") - # if not mgmt_ip_instance_id_map: - # msg = "Error occurred while retrieving device details (Device UUID) using the 'get_device_list' API " - # msg += "for the following device(s): {0}".format(ip_address_list) - # self.log(msg, "ERROR") - # self.module.fail_json(msg=msg) - return mgmt_ip_instance_id_map def get_device_ids_from_site(self, site_name, site_id): @@ -650,11 +644,6 @@ def get_device_ids_from_site(self, site_name, site_id): # Log an error message if any exception occurs during the process self.log("Unable to fetch the device(s) associated to the site '{0}' due to {1}".format(site_name, str(e)), "ERROR") - if not mgmt_ip_instance_id_map: - msg = "Error retrieving device details using the 'get_membership' API from Site: {0}".format(site_name) - self.log(msg, "ERROR") - self.module.fail_json(msg=msg) - return mgmt_ip_instance_id_map def get_device_id_list(self, ip_address_list, site_name): From 0ef2625d5f24090331ba088e61dc9b2ffd3cb601 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 30 May 2024 23:52:55 +0530 Subject: [PATCH 48/78] Handle the invalid input handle for which predefined options are present for SNMP, Syslog and Email destination and also fix the enhanced validation issue of server_address --- plugins/module_utils/dnac.py | 11 +-- ...ents_and_notifications_workflow_manager.py | 80 +++++++++++++++---- 2 files changed, 70 insertions(+), 21 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 4fa8ce086b..b378420177 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -572,11 +572,12 @@ def is_valid_server_address(self, server_address): # Define the regex for a valid hostname hostname_regex = re.compile( - r'^(?!-)' # Hostname must not start with a hyphen - r'[A-Za-z0-9-]{1,63}' # Hostname segment must be 1-63 characters long - r'(?!-)$' # Hostname segment must not end with a hyphen - r'(\.[A-Za-z0-9-]{1,63})*' # Each segment can be 1-63 characters long - r'(\.[A-Za-z]{2,6})$' # Top-level domain must be 2-6 alphabetic characters + r'^(' # Start of the string + r'([A-Za-z0-9]+([A-Za-z0-9-]*[A-Za-z0-9])?\.)+[A-Za-z]{2,6}|' # Domain name (e.g., example.com) + r'localhost|' # Localhost + r'(\d{1,3}\.)+\d{1,3}|' # Custom IPv4-like format (e.g., 2.2.3.31.3.4.4) + r'[A-Fa-f0-9:]+$' # IPv6 address (e.g., 2f8:192:3::40:41:41:42) + r')$' # End of the string ) # Check if the address is a valid hostname diff --git a/plugins/modules/events_and_notifications_workflow_manager.py b/plugins/modules/events_and_notifications_workflow_manager.py index 5eabd457c0..1acbc5fc7e 100644 --- a/plugins/modules/events_and_notifications_workflow_manager.py +++ b/plugins/modules/events_and_notifications_workflow_manager.py @@ -103,7 +103,8 @@ type: bool email_destination: description: Configure settings to send out emails from Cisco Catalyst Center. Also we can create or configure email destination in Cisco Catalyst - Center only once then later we can just modify it. + Center only once then later we can just modify it. This one is just used to configure the Primary and Secondary SMTP server while configuring + the email destination. It's not related to email event subscription notification. type: dict suboptions: primary_smtp_config: @@ -1048,6 +1049,13 @@ def collect_snmp_playbook_params(self, snmp_details): 'snmpVersion': snmp_details.get('snmp_version') } server_address = snmp_details.get('server_address') + snmp_version = playbook_params.get("snmpVersion") + + if snmp_version and snmp_version not in ["V2C", "V3"]: + self.status = "failed" + self.msg = "Invalid SNMP version '{0}' given in the playbook for configuring SNMP destination".format(snmp_version) + self.log(self.msg, "ERROR") + self.check_return_status() if server_address and not self.is_valid_server_address(server_address): self.status = "failed" @@ -1055,19 +1063,35 @@ def collect_snmp_playbook_params(self, snmp_details): self.log(self.msg, "ERROR") self.check_return_status() - if playbook_params.get('snmpVersion') == "V2C": + if snmp_version == "V2C": playbook_params['community'] = snmp_details.get('community') - elif playbook_params.get('snmpVersion') == "V3": + elif snmp_version == "V3": playbook_params['userName'] = snmp_details.get('username') playbook_params['snmpMode'] = snmp_details.get('mode') + mode = playbook_params['snmpMode'] + auth_type = snmp_details.get('auth_type') - if playbook_params['snmpMode'] == "AUTH_PRIVACY": - playbook_params['snmpAuthType'] = snmp_details.get('auth_type') + if not mode or (mode not in ["AUTH_PRIVACY", "AUTH_NO_PRIVACY", "NO_AUTH_NO_PRIVACY"]): + self.status = "failed" + self.msg = """Invalid SNMP Mode '{0}' given in the playbook for configuring SNMP destination. Please select one of + the mode - AUTH_PRIVACY, AUTH_NO_PRIVACY, NO_AUTH_NO_PRIVACY in the playbook""".format(mode) + self.log(self.msg, "ERROR") + self.check_return_status() + + if auth_type and auth_type not in ["SHA", "MD5"]: + self.status = "failed" + self.msg = """Invalid SNMP Authentication Type '{0}' given in the playbook for configuring SNMP destination. Please + select either SHA or MD5 as authentication type in the playbook""".format(auth_type) + self.log(self.msg, "ERROR") + self.check_return_status() + + if playbook_params.get("snmpMode") == "AUTH_PRIVACY": + playbook_params['snmpAuthType'] = auth_type playbook_params['authPassword'] = snmp_details.get('auth_password') playbook_params['snmpPrivacyType'] = snmp_details.get('privacy_type', 'AES128') playbook_params['privacyPassword'] = snmp_details.get('privacy_password') - elif playbook_params['snmpMode'] == "AUTH_NO_PRIVACY": - playbook_params['snmpAuthType'] = snmp_details.get('auth_type') + elif playbook_params.get("snmpMode") == "AUTH_NO_PRIVACY": + playbook_params['snmpAuthType'] = auth_type playbook_params['authPassword'] = snmp_details.get('auth_password') return playbook_params @@ -1570,11 +1594,19 @@ def collect_email_playbook_params(self, email_details): if email_details.get('primary_smtp_config'): primary_smtp_details = email_details.get('primary_smtp_config') + primary_smtp_type = primary_smtp_details.get('smtp_type', "DEFAULT") + if primary_smtp_type not in ["DEFAULT", "TLS", "SSL"]: + self.status = "failed" + self.msg = """Invalid Primary SMTP Type '{0}' given in the playbook for configuring primary smtp server. + Please select one of the type - DEFAULT, TLS, SSL in the playbook""".format(primary_smtp_type) + self.log(self.msg, "ERROR") + self.check_return_status() + playbook_params['primarySMTPConfig'] = {} playbook_params['primarySMTPConfig']['hostName'] = primary_smtp_details.get('server_address') - playbook_params['primarySMTPConfig']['smtpType'] = primary_smtp_details.get('smtp_type', "DEFAULT") + playbook_params['primarySMTPConfig']['smtpType'] = primary_smtp_type - if playbook_params['primarySMTPConfig']['smtpType'] == 'DEFAULT': + if primary_smtp_type == 'DEFAULT': playbook_params['primarySMTPConfig']['port'] = "25" else: playbook_params['primarySMTPConfig']['port'] = primary_smtp_details.get('port') @@ -1583,9 +1615,18 @@ def collect_email_playbook_params(self, email_details): if email_details.get('secondary_smtp_config'): secondary_smtp_details = email_details.get('secondary_smtp_config') + secondary_smtp_type = secondary_smtp_details.get('smtp_type', "DEFAULT") + + if secondary_smtp_type and secondary_smtp_type not in ["DEFAULT", "TLS", "SSL"]: + self.status = "failed" + self.msg = """Invalid Secondary SMTP Type '{0}' given in the playbook for configuring secondary smtp server. + Please select one of the type - DEFAULT, TLS, SSL in the playbook""".format(secondary_smtp_type) + self.log(self.msg, "ERROR") + self.check_return_status() + playbook_params['secondarySMTPConfig'] = {} playbook_params['secondarySMTPConfig']['hostName'] = secondary_smtp_details.get('server_address') - playbook_params['secondarySMTPConfig']['smtpType'] = secondary_smtp_details.get('smtp_type', "DEFAULT") + playbook_params['secondarySMTPConfig']['smtpType'] = secondary_smtp_type if playbook_params['secondarySMTPConfig']['smtpType'] == 'DEFAULT': playbook_params['secondarySMTPConfig']['port'] = "25" @@ -2167,18 +2208,20 @@ def get_diff_merged(self, config): return self regex_pattern = re.compile( - r'^https://' # Ensure the URL starts with "https://" like https://webhook.cisco.com - r'(([A-Za-z0-9-]+\.)+[A-Za-z]{2,6}|' # Domain name (e.g., example.com, webhook.cisco.com) + r'^https://' # Ensure the URL starts with "https://" + r'(' + r'(([A-Za-z0-9-]+\.)+[A-Za-z]{2,6}|' # Domain name (e.g., example.com) r'localhost|' # Localhost r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # IPv4 address (e.g., 192.168.0.1) - r'\[?[A-Fa-f0-9:]+\]?)' # IPv6 address (e.g., [2001:db8::1]) + r'\[[A-Fa-f0-9:]+\]' # IPv6 address (e.g., [2001:db8::1]) + r')' r'(:\d+)?' # Optional port (e.g., :8080) - r'(\/[A-Za-z0-9._~:/?#[@!$&\'()*+,;=-]*)?$' # Path and query (optional) + r'(\/[A-Za-z0-9._~:/?#[@!$&\'()*+,;=-]*)?$)' # Path and query (optional) ) url = webhook_params.get('url') # Check if the input string matches the pattern - if url and not re.match(regex_pattern, url): + if url and not regex_pattern.match(url): self.status = "failed" self.msg = """Given url '{0}' is invalid url for Creating/Updating Webhook destination. It must starts with 'https://' and follow the valid https url format.""".format(url) @@ -2286,7 +2329,7 @@ def get_diff_merged(self, config): if server_address and not self.is_valid_server_address(server_address): self.status = "failed" - self.msg = "Invalid server address '{0}' given in the playbook for configuring syslog destination".format(server_address) + self.msg = "Invalid server address '{0}' given in the playbook for configuring Syslog destination".format(server_address) self.log(self.msg, "ERROR") return self @@ -2477,25 +2520,30 @@ def get_diff_deleted(self, config): self.msg = "Deleting the Webhook destination is not supported in Cisco Catalyst Center because of API limitations" self.log(self.msg, "ERROR") self.result['changed'] = False + return self if config.get('email_destination'): self.status = "failed" self.msg = "Deleting the Email destination is not supported in Cisco Catalyst Center because of API limitations" self.log(self.msg, "ERROR") self.result['changed'] = False + return self if config.get('syslog_destination'): self.status = "failed" self.msg = "Deleting the Syslog destination is not supported in Cisco Catalyst Center because of API limitations" self.log(self.msg, "ERROR") self.result['changed'] = False + return self if config.get('snmp_destination'): self.status = "failed" self.msg = "Deleting the SNMP destination is not supported in Cisco Catalyst Center because of API limitations" self.log(self.msg, "ERROR") self.result['changed'] = False + return self + # Delete ITSM Integration setting from Cisco Catalyst Center if config.get('itsm_setting'): itsm_details = self.want.get('itsm_details') itsm_name = itsm_details.get('instance_name') From 0ee5f6f6aebec1a8a3c70949985b05408f81c23e Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Thu, 30 May 2024 15:32:38 -0700 Subject: [PATCH 49/78] network_compliance bug fixes --- .../network_compliance_workflow_manager.py | 57 +++++++++++++++---- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/plugins/modules/network_compliance_workflow_manager.py b/plugins/modules/network_compliance_workflow_manager.py index cb335f5570..ef13179ad0 100644 --- a/plugins/modules/network_compliance_workflow_manager.py +++ b/plugins/modules/network_compliance_workflow_manager.py @@ -436,6 +436,7 @@ def validate_run_compliance_paramters(self, mgmt_ip_instance_id_map, run_complia - If `run_compliance` is set and `run_compliance_categories` is not, full compliance checks are triggered. - If both `run_compliance` and `run_compliance_categories` are set, compliance checks are triggered for specific categories. """ + # Initializing empty dicts/lists run_compliance_params = {} compliance_detail_params = {} valid_categories = ["INTENT", "RUNNING_CONFIG", "IMAGE", "PSIRT", "EOX", "NETWORK_SETTINGS"] @@ -478,6 +479,32 @@ def validate_run_compliance_paramters(self, mgmt_ip_instance_id_map, run_complia # compliance_detail_params compliance_detail_params["deviceUuids"] = ",".join(list(mgmt_ip_instance_id_map.values())) + # Verify if there are any devices with Compliance Status of "IN_PROGRESS" + if run_compliance_params: + device_in_progress = [] + + response = self.get_compliance_detail(compliance_detail_params) + if not response: + msg = "Error occurred when retrieving Compliance Report to identify if there are devices with 'IN_PROGRESS' status" + msg += "is required on device(s): {0}".format(list(mgmt_ip_instance_id_map.keys())) + self.log(msg) + self.module.fail_json(msg) + + # Iterate through the response to identify devices with 'IN_PROGRESS' status + for device in response: + if device["status"] == "IN_PROGRESS": + device_in_progress.append(device["deviceUuid"]) + self.log("Devices currently with a Compliance Status of 'IN_PROGRESS': {0}".format(device_in_progress), "DEBUG") + + if device_in_progress: + # Update run_compliance_params to exclude devices with 'IN_PROGRESS' status + run_compliance_params["deviceUuids"] = [device_id for device_id in mgmt_ip_instance_id_map.values() if device_id not in device_in_progress] + self.log( + "Updated the run_compliance_params to exclude devices with a compliance status of 'IN_PROGRESS'." + "run_compliance_params: {0}".format(run_compliance_params), + "DEBUG" + ) + return run_compliance_params, compliance_detail_params def site_exists(self, site_name): @@ -721,14 +748,8 @@ def is_sync_required(self, modified_response, mgmt_ip_instance_id_map): msg = "Device(s) {0} are already compliant with the RUNNING_CONFIG compliance type. Therefore, {1} is not required.".format( list(mgmt_ip_instance_id_map.keys()), task_name) required = False - elif len(categorized_devices["NON_COMPLIANT"]) != len(mgmt_ip_instance_id_map): - required = False - msg = ("The operation {0} cannot be performed on one or more of the devices " - "{1} because the status of the RUNNING_CONFIG compliance type is not " - "as expected; it should be NON_COMPLIANT." - ).format(task_name, list(mgmt_ip_instance_id_map.keys())) - return required, msg + return required, msg, categorized_devices def get_want(self, config): """ @@ -810,10 +831,26 @@ def get_want(self, config): self.module.fail_json(msg) compliance_details = self.modify_compliance_response(response, mgmt_ip_instance_id_map) - required, msg = self.is_sync_required(compliance_details, mgmt_ip_instance_id_map) + required, self.msg, categorized_devices = self.is_sync_required(compliance_details, mgmt_ip_instance_id_map) + self.log("Is Sync Requied: {0} {1}".format(required, self.msg), "DEBUG") if not required: - self.log(msg, "ERROR") - self.module.fail_json(msg) + self.update_result("success", False, self.msg, "INFO") + self.module.exit_json(**self.result) + + # Get the device IDs of devices in the "OTHER" category and "COMPLIANT" category + other_device_ids = categorized_devices.get("OTHER", {}).keys() + compliant_device_ids = categorized_devices.get("COMPLIANT", {}).keys() + excluded_device_ids = set(other_device_ids) | set(compliant_device_ids) + + if excluded_device_ids: + # Exclude devices in the "OTHER" category from sync_device_config_params + sync_device_config_params["deviceId"] = [device_id for device_id in mgmt_ip_instance_id_map.values() if device_id not in excluded_device_ids] + self.log( + "Skipping these devices because their compliance status is not 'NON_COMPLIANT': {0}".format( + categorized_devices.get("OTHER") + ), + "WARNING" + ) # Construct the "want" dictionary containing the desired state parameters want = {} From 6dfbf6d65f3114d7d14a9e650381d40a0af9cf3b Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Thu, 30 May 2024 15:48:26 -0700 Subject: [PATCH 50/78] network_compliance bug fixes --- plugins/modules/network_compliance_workflow_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/network_compliance_workflow_manager.py b/plugins/modules/network_compliance_workflow_manager.py index ef13179ad0..1180c42cdb 100644 --- a/plugins/modules/network_compliance_workflow_manager.py +++ b/plugins/modules/network_compliance_workflow_manager.py @@ -841,14 +841,14 @@ def get_want(self, config): other_device_ids = categorized_devices.get("OTHER", {}).keys() compliant_device_ids = categorized_devices.get("COMPLIANT", {}).keys() excluded_device_ids = set(other_device_ids) | set(compliant_device_ids) - + if excluded_device_ids: # Exclude devices in the "OTHER" category from sync_device_config_params sync_device_config_params["deviceId"] = [device_id for device_id in mgmt_ip_instance_id_map.values() if device_id not in excluded_device_ids] self.log( "Skipping these devices because their compliance status is not 'NON_COMPLIANT': {0}".format( categorized_devices.get("OTHER") - ), + ), "WARNING" ) From fbaf823687446113ace5a4aefd2a9ab7016c9709 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 31 May 2024 15:09:18 +0530 Subject: [PATCH 51/78] update the regex pattern in webhook destination as it's not accepting ipv6 --- .../events_and_notifications_workflow_manager.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/plugins/modules/events_and_notifications_workflow_manager.py b/plugins/modules/events_and_notifications_workflow_manager.py index 1acbc5fc7e..964a5a3493 100644 --- a/plugins/modules/events_and_notifications_workflow_manager.py +++ b/plugins/modules/events_and_notifications_workflow_manager.py @@ -2210,13 +2210,16 @@ def get_diff_merged(self, config): regex_pattern = re.compile( r'^https://' # Ensure the URL starts with "https://" r'(' - r'(([A-Za-z0-9-]+\.)+[A-Za-z]{2,6}|' # Domain name (e.g., example.com) + r'(([A-Za-z0-9-*.&@]+\.)+[A-Za-z]{2,6})|' # Domain name with wildcards and special characters r'localhost|' # Localhost - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # IPv4 address (e.g., 192.168.0.1) - r'\[[A-Fa-f0-9:]+\]' # IPv6 address (e.g., [2001:db8::1]) + r'(?:(?:\d{1,3}\.){3}\d{1,3}\b\.?)' # Partial or complete IPv4 address with optional trailing dot + r'(\[[A-Fa-f0-9:]+\])?' # Optional IPv6 address in square brackets (e.g., [2001:db8::1]) + r'|' # Alternation for different valid segments + r'([A-Za-z-_.&@]+)' # Hostname with allowed special characters r')' - r'(:\d+)?' # Optional port (e.g., :8080) - r'(\/[A-Za-z0-9._~:/?#[@!$&\'()*+,;=-]*)?$)' # Path and query (optional) + r'(:\d+)?' # Optional port + r'(\/[A-Za-z0-9._~:/?#[@!$&\'()*+,;=-]*)?' # Optional path + r'$' # End of the string ) url = webhook_params.get('url') From d444556b29d1a079281b3af64f090f4b62c4d13e Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 31 May 2024 23:06:31 +0530 Subject: [PATCH 52/78] Added the code for creating/updating/deleting events subscription notification with specified destination and added the playbook and documentation with examples as well --- ...nts_and_notifications_workflow_manager.yml | 37 +- ...ents_and_notifications_workflow_manager.py | 2430 ++++++++++++++++- 2 files changed, 2417 insertions(+), 50 deletions(-) diff --git a/playbooks/events_and_notifications_workflow_manager.yml b/playbooks/events_and_notifications_workflow_manager.yml index a30918474b..9284912efe 100644 --- a/playbooks/events_and_notifications_workflow_manager.yml +++ b/playbooks/events_and_notifications_workflow_manager.yml @@ -21,13 +21,7 @@ config_verify: true state: merged config: - - syslog_destination: - name: "{{item.syslog_destination.name}}" - description: "{{item.syslog_destination.description}}" - server_address: "{{item.syslog_destination.server_address}}" - protocol: "{{item.syslog_destination.protocol}}" - port: "{{item.syslog_destination.port}}" - webhook_destination: + - webhook_destination: name: "{{item.webhook_destination.name}}" description: "{{item.webhook_destination.description}}" url: "{{item.webhook_destination.url}}" @@ -40,6 +34,13 @@ primary_smtp_config: server_address: "{{item.email_destination.primary_smtp_config.server_address}}" port: "{{item.email_destination.primary_smtp_config.port}}" + username: "{{item.email_destination.primary_smtp_config.username}}" + syslog_destination: + name: "{{item.syslog_destination.name}}" + description: "{{item.syslog_destination.description}}" + server_address: "{{item.syslog_destination.server_address}}" + protocol: "{{item.syslog_destination.protocol}}" + port: "{{item.syslog_destination.port}}" snmp_destination: name: "{{item.snmp_destination.name}}" description: "{{item.snmp_destination.description}}" @@ -59,6 +60,28 @@ url: "{{item.itsm_setting.connection_settings.url}}" username: "{{item.itsm_setting.connection_settings.username}}" password: "{{item.itsm_setting.connection_settings.password}}" + webhook_event_notification: + notification_name: "{{item.webhook_event_notification.notification_name}}" + description: "{{item.webhook_event_notification.description}}" + webhook_dest_name: "{{item.webhook_event_notification.webhook_dest_name}}" + events_name_list: "{{item.webhook_event_notification.events_name_list}}" + site_name_list: "{{item.webhook_event_notification.site_name_list}}" + email_event_notification: + notification_name: "{{item.email_event_notification.notification_name}}" + description: "{{item.email_event_notification.description}}" + events_name_list: "{{item.email_event_notification.events_name_list}}" + site_name_list: "{{item.email_event_notification.site_name_list}}" + from_email_address: "{{item.email_event_notification.from_email_address}}" + to_email_addresses: "{{item.email_event_notification.to_email_addresses}}" + subject: "{{item.email_event_notification.subject}}" + instance_name: "{{item.email_event_notification.instance_name}}" + instance_description: "{{item.email_event_notification.instance_description}}" + syslog_event_notification: + notification_name: "{{item.syslog_event_notification.notification_name}}" + description: "{{item.syslog_event_notification.description}}" + syslog_dest_name: "{{item.syslog_event_notification.syslog_dest_name}}" + events_name_list: "{{item.syslog_event_notification.events_name_list}}" + site_name_list: "{{item.syslog_event_notification.site_name_list}}" with_items: "{{ events_notification }}" tags: diff --git a/plugins/modules/events_and_notifications_workflow_manager.py b/plugins/modules/events_and_notifications_workflow_manager.py index 964a5a3493..065d39fd01 100644 --- a/plugins/modules/events_and_notifications_workflow_manager.py +++ b/plugins/modules/events_and_notifications_workflow_manager.py @@ -295,7 +295,187 @@ description: The password associated with the username for API authentication. It is recommended to handle this data securely. type: str required: True - + webhook_event_notification: + description: Dictionary containing the details for creating/updating the Webhook Event subscription notification in Cisco Catalyst + Center. + type: dict + suboptions: + notification_name: + description: Name of the Webhook event subscription notification . + type: str + required: True + description: + description: A brief explanation detailing the purpose of the email events subscription notification. + type: str + required: True + version: + description: Version of the event subscription notification . + type: str + webhook_dest_name: + description: Name of the Webhook destination that is been configured to send the event subscription notification. And although + it's list but we have to give the name of only one Webhook destination. + type: str + required: True + events_name_list: + description: List of the event names to be subscribed while configuring notification (For example, ["AP Flap", "AP Reboot Crash"]). + type: list + elements: str + required: True + domain: + description: Name of the domain for the events (For example, Know Your Network, Connectivity etc.). + type: str + sub_domains: + description: List of subdomains within the specified domain (For example, ["Wireless", "Applications"]). + type: list + elements: str + event_types: + description: List of event types to be included in the subscription of a notification (For example, ["APP", "NETWORK"]). + type: list + elements: str + event_categories: + description: List of event categories to be included in the subscription of a notification + (For example, ["WARN", "INFO", "ERROR", "ALERT", "TASK_COMPLETE", "TASK_FAILURE"]). + type: list + elements: str + event_severities: + description: List of event severities to be included in the subscription of a notification (For example, ["1", "2", "3"]). + type: list + elements: str + event_sources: + description: List of event sources to be included in the subscription of a notification. + type: list + elements: str + site_name_list: + description: List of site names where events are to be included in the subscription of a notification. + (For example, ["Global/India", "Global/USA"]). + type: list + elements: str + email_event_notification: + description: Dictionary containing the details for creating/updating the Email Event subscription notification in Cisco Catalyst + Center. Here you can create the email subscription notification as well as the create/update the email instance as well. + type: dict + suboptions: + notification_name: + description: Name of the Webhook event subscription notification . + type: str + required: True + description: + description: A brief explanation detailing the purpose of the email events subscription notification. + type: str + required: True + version: + description: Version of the event subscription notification . + type: str + events_name_list: + description: List of the event names to be subscribed while configuring notification (For example, ["AP Flap", "AP Reboot Crash"]). + type: list + elements: str + required: True + from_email_address: + description: The email address from which the notification is sent. + type: str + required: True + to_email_addresses: + description: A list of email addresses to which the notification is sent. + type: list + elements: str + required: True + subject: + description: The subject line of the notification email. + type: str + required: True + instance_name: + description: The name of the email instance related to the notification. + type: str + required: True + instance_description: + description: A brief description of the email instance releated to the notification. + type: str + required: True + domain: + description: Name of the domain for the events (For example, Know Your Network, Connectivity etc.). + type: str + sub_domains: + description: List of subdomains within the specified domain (For example, ["Wireless", "Applications"]). + type: list + elements: str + event_types: + description: List of event types to be included in the subscription of a notification (For example, ["APP", "NETWORK"]). + type: list + elements: str + event_categories: + description: List of event categories to be included in the subscription of a notification + (For example, ["WARN", "INFO", "ERROR", "ALERT", "TASK_COMPLETE", "TASK_FAILURE"]). + type: list + elements: str + event_severities: + description: List of event severities to be included in the subscription of a notification (For example, ["1", "2", "3"]). + type: list + elements: str + event_sources: + description: List of event sources to be included in the subscription of a notification. + type: list + elements: str + site_name_list: + description: List of site names where events are to be included in the subscription of a notification. + (For example, ["Global/India", "Global/USA"]). + type: list + elements: str + syslog_event_notification: + description: Dictionary containing the details for creating/updating the Syslog Event subscription notification in Cisco Catalyst + Center. + type: dict + suboptions: + notification_name: + description: Name of the Syslog event subscription notification . + type: str + required: True + description: + description: A brief explanation detailing the purpose of the syslog events subscription notification. + type: str + required: True + version: + description: Version of the event subscription notification . + type: str + syslog_dest_name: + description: Name of the Syslog destination that is been configured to send the event subscription notification. And although + it's list but we have to give the name of only one Syslog destination. + type: str + required: True + events_name_list: + description: List of the event names to be subscribed while configuring notification (For example, ["AP Flap", "AP Reboot Crash"]). + type: list + elements: str + required: True + domain: + description: Name of the domain for the events (For example, Know Your Network, Connectivity etc.). + type: str + sub_domains: + description: List of subdomains within the specified domain (For example, ["Wireless", "Applications"]). + type: list + elements: str + event_types: + description: List of event types to be included in the subscription of a notification (For example, ["APP", "NETWORK"]). + type: list + elements: str + event_categories: + description: List of event categories to be included in the subscription of a notification + (For example, ["WARN", "INFO", "ERROR", "ALERT", "TASK_COMPLETE", "TASK_FAILURE"]). + type: list + elements: str + event_severities: + description: List of event severities to be included in the subscription of a notification (For example, ["1", "2", "3"]). + type: list + elements: str + event_sources: + description: List of event sources to be included in the subscription of a notification. + type: list + elements: str + site_name_list: + description: List of site names where events are to be included in the subscription of a notification. + (For example, ["Global/India", "Global/USA"]). + type: list + elements: str requirements: - dnacentersdk >= 2.5.5 @@ -530,6 +710,130 @@ url: "http/catalystcenterupdate.com" password: "catalyst@123" +- name: Creating Webhook Notification with the list of names of subscribed events in the system. + cisco.dnac.events_and_notifications_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - webhook_event_notification: + notification_name: "Webhook Notification." + description: "Notification for webhook events subscription" + webhook_dest_name: "Webhook Demo" + events_name_list: ["AP Flap", "AP Reboot Crash"] + site_name_list: ["Global/India", "Global/USA"] + +- name: Updating Webhook Notification with the list of names of subscribed events in the system. + cisco.dnac.events_and_notifications_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - webhook_event_notification: + notification_name: "Webhook Notification." + description: "Updated notification for webhook events subscription" + events_name_list: ["AP Flap", "AP Reboot Crash"] + site_name_list: ["Global/India", "Global/USA", "Global/China"] + +- name: Creating Email Notification with the list of names of subscribed events in the system. + cisco.dnac.events_and_notifications_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - email_event_notification: + notification_name: "Email Notification" + description: "Notification description for email subscription creation" + events_name_list: ["AP Flap", "AP Reboot Crash"] + site_name_list: ["Global/India", "Global/USA"] + from_email_address: "catalyst@cisco.com" + to_email_addresses: ["test@cisco.com", "demo@cisco.com"] + subject: "Mail test" + instance_name: Email Instance test + +- name: Updating Email Notification with the list of names of subscribed events in the system. + cisco.dnac.events_and_notifications_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - email_event_notification: + notification_name: "Email Notification" + description: "Notification description for email subscription updation" + events_name_list: ["AP Flap", "AP Reboot Crash"] + site_name_list: ["Global/India", "Global/USA"] + from_email_address: "catalyst@cisco.com" + to_email_addresses: ["test@cisco.com", "demo@cisco.com", "update@cisco.com"] + subject: "Mail test for updation" + instance_name: Email Instance test + +- name: Creating Syslog Notification with the list of names of subscribed events in the system. + cisco.dnac.events_and_notifications_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - syslog_event_notification: + notification_name: "Syslog Notification." + description: "Notification for syslog events subscription" + syslog_dest_name: "Syslog Demo" + events_name_list: ["AP Flap", "AP Reboot Crash"] + site_name_list: ["Global/India", "Global/USA"] + +- name: Updating Syslog Notification with the list of names of subscribed events in the system. + cisco.dnac.events_and_notifications_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: merged + config: + - syslog_event_notification: + notification_name: "Syslog Notification." + description: "Updated notification for syslog events subscription" + events_name_list: ["AP Flap", "AP Reboot Crash"] + site_name_list: ["Global/India", "Global/USA", "Global/China"] + - name: Deleting ITSM Integration Setting with given name from the system. cisco.dnac.events_and_notifications_workflow_manager: dnac_host: "{{dnac_host}}" @@ -546,6 +850,54 @@ - itsm_setting: instance_name: "ITSM test" +- name: Deleting Webhook Events Subscription Notification with given name from the system. + cisco.dnac.events_and_notifications_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: deleted + config: + - webhook_event_notification: + notification_name: "Webhook Notification" + +- name: Deleting Email Events Subscription Notification with given name from the system. + cisco.dnac.events_and_notifications_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: deleted + config: + - email_event_notification: + notification_name: "Email Notification" + +- name: Deleting Syslog Events Subscription Notification with given name from the system. + cisco.dnac.events_and_notifications_workflow_manager: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log_level: "{{dnac_log_level}}" + dnac_log: False + state: deleted + config: + - syslog_event_notification: + notification_name: "Syslog Notification" + """ RETURN = r""" @@ -672,6 +1024,55 @@ def validate_input(self): 'password': {'type': 'str'}, }, }, + 'webhook_event_notification': { + 'type': 'dict', + 'notification_name': {'type': 'str'}, + 'version': {'type': 'str'}, + 'description': {'type': 'str'}, + 'webhook_dest_name': {'type': 'str'}, + 'events_name_list': {'type': 'list', 'elements': 'str'}, + 'domain': {'type': 'str'}, + 'sub_domains': {'type': 'list', 'elements': 'str'}, + 'event_types': {'type': 'list', 'elements': 'str'}, + 'event_categories': {'type': 'list', 'elements': 'str'}, + 'event_severities': {'type': 'list', 'elements': 'str'}, + 'event_sources': {'type': 'list', 'elements': 'str'}, + 'site_name_list': {'type': 'list', 'elements': 'str'}, + }, + 'email_event_notification': { + 'type': 'dict', + 'notification_name': {'type': 'str'}, + 'version': {'type': 'str'}, + 'description': {'type': 'str'}, + 'from_email_address': {'type': 'str'}, + 'to_email_addresses': {'type': 'list', 'elements': 'str'}, + 'subject': {'type': 'str'}, + 'instance_name': {'type': 'str'}, + 'instance_description': {'type': 'str'}, + 'events_name_list': {'type': 'list', 'elements': 'str'}, + 'domain': {'type': 'str'}, + 'sub_domains': {'type': 'list', 'elements': 'str'}, + 'event_types': {'type': 'list', 'elements': 'str'}, + 'event_categories': {'type': 'list', 'elements': 'str'}, + 'event_severities': {'type': 'list', 'elements': 'str'}, + 'event_sources': {'type': 'list', 'elements': 'str'}, + 'site_name_list': {'type': 'list', 'elements': 'str'}, + }, + 'syslog_event_notification': { + 'type': 'dict', + 'notification_name': {'type': 'str'}, + 'version': {'type': 'str'}, + 'description': {'type': 'str'}, + 'syslog_dest_name': {'type': 'str'}, + 'events_name_list': {'type': 'list', 'elements': 'str'}, + 'domain': {'type': 'str'}, + 'sub_domains': {'type': 'list', 'elements': 'str'}, + 'event_types': {'type': 'list', 'elements': 'str'}, + 'event_categories': {'type': 'list', 'elements': 'str'}, + 'event_severities': {'type': 'list', 'elements': 'str'}, + 'event_sources': {'type': 'list', 'elements': 'str'}, + 'site_name_list': {'type': 'list', 'elements': 'str'}, + }, } # Validate device params @@ -738,6 +1139,21 @@ def get_have(self, config): if itsm_setting: have['itsm_setting'] = itsm_setting + if config.get('syslog_event_notification'): + syslog_subscription_notifications = self.get_syslog_notification_details() + if syslog_subscription_notifications: + have['syslog_subscription_notifications'] = syslog_subscription_notifications + + if config.get('webhook_event_notification'): + webhook_subscription_notifications = self.get_webhook_notification_details() + if webhook_subscription_notifications: + have['webhook_subscription_notifications'] = webhook_subscription_notifications + + if config.get('email_event_notification'): + email_subscription_notifications = self.get_email_notification_details() + if email_subscription_notifications: + have['email_subscription_notifications'] = email_subscription_notifications + self.have = have self.log("Current State (have): {0}".format(str(self.have)), "INFO") @@ -774,6 +1190,15 @@ def get_want(self, config): if config.get('itsm_setting'): want['itsm_details'] = config.get('itsm_setting') + if config.get('webhook_event_notification'): + want['webhook_event_notification'] = config.get('webhook_event_notification') + + if config.get('email_event_notification'): + want['email_event_notification'] = config.get('email_event_notification') + + if config.get('syslog_event_notification'): + want['syslog_event_notification'] = config.get('syslog_event_notification') + self.want = want self.msg = "Successfully collected all parameters from the playbook " self.status = "success" @@ -869,6 +1294,12 @@ def add_syslog_destination(self, syslog_details): server_address = syslog_details.get('server_address') protocol = syslog_details.get('protocol') + if not self.is_valid_ipv4(server_address): + self.status = "failed" + self.msg = "Invalid server adderess '{0}' given in the playbook for configuring syslog destination".format(server_address) + self.log(self.msg, "ERROR") + return self + if not protocol: self.status = "failed" self.msg = "Protocol is needed while configuring the syslog destionation with name '{0}' in Cisco Catalyst Center".format(name) @@ -961,6 +1392,13 @@ def update_syslog_destination(self, syslog_details, syslog_details_in_ccc): self.log(self.msg, "ERROR") return self + server_address = update_syslog_params.get('host') + if not self.is_valid_ipv4(server_address): + self.status = "failed" + self.msg = "Invalid server adderess '{0}' given in the playbook for updating syslog destination".format(server_address) + self.log(self.msg, "ERROR") + return self + response = self.dnac._exec( family="event_management", function='update_syslog_destination', @@ -2158,57 +2596,1626 @@ def delete_itsm_integration_setting(self, itsm_name, itsm_id): return self - def get_diff_merged(self, config): + def get_syslog_notification_details(self): """ - Processes the configuration difference and merges them into Cisco Catalyst Center. - This method updates Cisco Catalyst Center configurations based on the differences detected - between the desired state (`want`) and the current state (`have`). It handles different - types of configurations such as syslog, SNMP, REST webhook, email, and ITSM settings. + Retrieves the details of a Syslog Event Notification subscription from the Cisco Catalyst Center. Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - config (dict): A dictionary containing various destination settings that may include - syslog destination, SNMP destination, REST webhook destination, - email destination, and ITSM settings. Each key should point to a dictionary - that defines specific configuration for that setting type. - Return: - self (object): Returns the instance itself after potentially modifying internal state to reflect - the status of operation, messages to log, and response details. + Returns: + dict or None: A dictionary containing the details of the Syslog Event Notification subscription if found. + Returns None if no subscription is found or if an error occurs during the API call. Description: - This method acts as a controller that delegates specific tasks such as adding or updating - configurations for syslog, SNMP, REST webhook, email, and ITSM settings in the Cisco Catalyst - Center. It ensures required parameters are present, validates them, and calls the appropriate - methods to add or update the configurations. Error handling is included to manage any exceptions - or invalid configurations, updating the internal state to reflect these errors. + This function calls an API to fetch the details of a specified Syslog Event Notification subscription. If the + subscription exists, it returns the response containing the subscription details. If no subscription is found + or an error occurs, it logs the appropriate message and handles the exception accordingly. """ - # Create/Update Rest Webhook destination in Cisco Catalyst Center - if config.get('webhook_destination'): - webhook_details = self.want.get('webhook_details') - destination_name = webhook_details.get('name') - - if not destination_name: - self.status = "failed" - self.msg = "Name is required parameter for adding/updating Webhook destination for creating/updating the event." - self.log(self.msg, "ERROR") - return self + try: + response = self.dnac._exec( + family="event_management", + function='get_syslog_event_subscriptions', + ) + self.log("Received API response from 'get_syslog_event_subscriptions': {0}".format(str(response)), "DEBUG") - is_destination_exist = False - for webhook_dict in self.have.get('webhook_destinations'): - if webhook_dict['name'] == destination_name: - webhook_dest_detail_in_ccc = webhook_dict - is_destination_exist = True - break - webhook_params = self.collect_webhook_playbook_params(webhook_details) + if not response: + self.log("There is no Syslog Event Notification present in Cisco Catalyst Center.", "INFO") + return response - if webhook_params.get('method') not in ["POST", "PUT"]: - self.status = "failed" - self.msg = """Invalid Webhook method name '{0}' for creating/updating Webhook destination in Cisco Catalyst Center. - Select one of the following method 'POST/PUT'.""".format(webhook_params.get('method')) - self.log(self.msg, "ERROR") - return self + return response - regex_pattern = re.compile( - r'^https://' # Ensure the URL starts with "https://" + except Exception as e: + self.status = "failed" + self.msg = """Error while getting the details of Syslog Event subscription Notification present in + Cisco Catalyst Center: {0}""".format(str(e)) + self.log(self.msg, "ERROR") + self.check_return_status() + + def get_syslog_subscription_detail(self, syslog_dest_name): + """ + Retrieves the details of a specific Syslog destination subscription from the Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + syslog_dest_name (str): The name of the Syslog destination for which details needs to be fetched. + Returns: + dict or list: A dictionary containing the details of the Syslog destination subscription if found. + Returns an empty list if no destination is found or if an error occurs during the API call. + Description: + This function calls an API to fetch the details of all Syslog destination from the Cisco Catalyst Center. + It then searches for a subscription that matches the given `syslog_dest_name`. If a match is found, it returns + details of the matching subscription. If no match is found or if an error occurs, it logs the appropriate message + and handles the exception accordingly. + """ + + try: + response = self.dnac._exec( + family="event_management", + function='get_syslog_subscription_details', + ) + self.log("Received API response from 'get_syslog_subscription_details': {0}".format(str(response)), "DEBUG") + sys_destination_details = [] + + if not response: + self.log("There is no Syslog destination present in Cisco Catalyst Center.", "INFO") + return sys_destination_details + + for dest in response: + if dest["name"] == syslog_dest_name: + return dest + self.log("There is no Syslog destination with given name '{0}' present in Cisco Catalyst Center.".format(syslog_dest_name), "INFO") + + return sys_destination_details + + except Exception as e: + self.status = "failed" + self.msg = """Error while getting the details of Syslog Subscription with given name '{0}' present in + Cisco Catalyst Center: {1}""".format(syslog_dest_name, str(e)) + self.log(self.msg, "ERROR") + self.check_return_status() + + def get_event_ids(self, events_name_list): + """ + Retrieves the event IDs for a given list of event names from the Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + events_name_list (list of str): A list of event names for which the event IDs need to be retrieved. + Returns: + list of str: A list of event IDs corresponding to the provided event names. If an event name is not + found, it is skipped. + Description: + This function iterates over a list of event names and calls an API to fetch the details of each event from + the Cisco Catalyst Center. If the event is found, its event ID is extracted and added to the list of event IDs. + The function logs messages for successfulAPI responses, missing events, and any errors encountered during the + process. The final list of event IDs is returned. + """ + + event_ids = [] + + for event_name in events_name_list: + try: + response = self.dnac._exec( + family="event_management", + function='get_eventartifacts', + op_modifies=True, + params={"search": event_name} + ) + self.log("Received API response from 'get_eventartifacts': {0}".format(str(response)), "DEBUG") + + if not response: + self.log("There is no Event with name '{0}' present in Cisco Catalyst Center.".format(event_name), "INFO") + continue + + response = response[0] + event_payload = response.get('eventPayload') + if event_payload: + event_id = event_payload.get('eventId') + event_ids.append(event_id) + + except Exception as e: + self.msg = """Error while getting the details of Event with given name '{0}' present in + Cisco Catalyst Center: {1}""".format(event_name, str(e)) + self.log(self.msg, "ERROR") + + return event_ids + + def get_site_ids(self, site_name_list): + """ + Retrieves the site IDs for a given list of site names from the Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + site_name_list (list of str): A list of site names for which the site IDs need to be retrieved. + Returns: + list of str: A list of site IDs corresponding to the provided site names. If a site name is not + found, it is skipped and return empty list. + Description: + This function iterates over a list of site names and calls an API to fetch the details of each site + from the Cisco Catalyst Center. If the site is found, its site ID is extracted and added to the list + of site IDs. The function logs messages for successful API responses, missing sites, and any errors + encountered during the process. The final list of site IDs is returned. + """ + + site_ids = [] + for site_name in site_name_list: + try: + response = self.dnac._exec( + family="sites", + function='get_site', + op_modifies=True, + params={"name": site_name}, + ) + self.log("Received API response from 'get_site': {0}".format(str(response)), "DEBUG") + response = response.get('response') + if not response: + self.log("There is no Site with name '{0}' present in Cisco Catalyst Center.".format(site_name), "INFO") + continue + site_id = response[0].get("id") + site_ids.append(site_id) + + except Exception as e: + self.msg = """Error while getting the details of Site with given name '{0}' present in + Cisco Catalyst Center: {1}""".format(site_name, str(e)) + self.log(self.msg, "ERROR") + + return site_ids + + def collect_syslog_notification_playbook_params(self, syslog_notification_details): + """ + Collects and prepares parameters for creating or updating a Syslog Event Notification. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + syslog_notification_details (dict): A dictionary containing the details required for creating or updating + the Syslog Event Notification. + Returns: + list of dict: A list containing a dictionary with the parameters for creating the Syslog Event Notification. + Description: + This function collects and structures the necessary parameters for creating or updating a Syslog Event Notification. + It fetches additional details such as instance IDs and connector types from the Cisco Catalyst Center + and prepares the subscription endpoints and filters. The function handles missing or incorrect details by logging + appropriate messages and adjusting the status and returns a list containing required parameter. + """ + + syslog_notification_params = [] + name = syslog_notification_details.get('notification_name') + playbook_params = { + 'name': name, + 'description': syslog_notification_details.get('description'), + 'version': syslog_notification_details.get('version'), + 'subscriptionEndpoints': [], + 'filter': {} + + } + # Collect the Instance ID of the syslog destination + syslog_dest_name = syslog_notification_details.get('syslog_dest_name') + + if syslog_dest_name: + subscription_details = self.get_syslog_subscription_detail(syslog_dest_name) + + if not subscription_details: + self.status = "failed" + self.msg = """Unable to create/update the syslog event notification '{0}' as syslog desination '{1}' is not configured or + present in Cisco Catalyst Center""".format(name, syslog_dest_name) + self.log(self.msg, "ERROR") + self.check_return_status() + + instance_id = subscription_details.get('instanceId') + connector_type = subscription_details.get('connectorType') + temp_subscript_endpoint = { + "instanceId": instance_id, + "subscriptionDetails": { + "connectorType": connector_type + } + } + playbook_params["subscriptionEndpoints"].append(temp_subscript_endpoint) + + events_name_list = syslog_notification_details.get('events_name_list') + if events_name_list: + events_ids = self.get_event_ids(events_name_list) + + if not events_ids: + self.status = "failed" + self.msg = "Unable to create/update Syslog event notification as given events name '{0}' are incorrect.".format(str(events_name_list)) + self.log(self.msg, "ERROR") + self.check_return_status() + + playbook_params["filter"]["eventIds"] = events_ids + + domain = syslog_notification_details.get("domain") + sub_domains = syslog_notification_details.get("sub_domains") + if domain and sub_domains: + playbook_params["filter"]["domainsSubdomains"] = [] + domain_dict = { + "domain": domain, + "subDomains": sub_domains + } + playbook_params["filter"]["domainsSubdomains"].append(domain_dict) + + event_types = syslog_notification_details.get("event_types") + if event_types: + playbook_params["filter"]["types"] = event_types + + event_categories = syslog_notification_details.get("event_categories") + if event_categories: + playbook_params["filter"]["categories"] = event_categories + + event_severities = syslog_notification_details.get("event_severities") + if event_severities: + playbook_params["filter"]["severities"] = event_severities + + event_sources = syslog_notification_details.get("event_sources") + if event_sources: + playbook_params["filter"]["sources"] = event_sources + + site_name_list = syslog_notification_details.get("site_name_list") + if site_name_list: + site_ids = self.get_site_ids(site_name_list) + + if not site_ids: + self.msg = "Unable to find the Site ID's for the given site(s) - '{0}' in the playbook's input.".format(str(site_name_list)) + self.log(self.msg, "INFO") + + playbook_params["filter"]["siteIds"] = site_ids + syslog_notification_params.append(playbook_params) + + return syslog_notification_params + + def mandatory_syslog_notification_parameter_check(self, syslog_notification_params): + """ + Checks for the presence of mandatory parameters required for adding a Syslog Event Notification. + Args: + syslog_notification_params (list of dict): A list containing a single dictionary with the parameters + for the Syslog Event Notification. + Returns: + self: The instance of the class with updated status and message if any required parameter is missing. + Description: + This function verifies the presence of required parameters for creating or updating a Syslog Event Notification. + If any required parameter is absent, it logs an error message, updates the status to "failed", + and sets the message attribute. It then returns the instance of the class with the updated status and message. + """ + + required_params_absent = [] + syslog_notification_params = syslog_notification_params[0] + notification_name = syslog_notification_params.get("name") + description = syslog_notification_params.get("description") + + if not notification_name: + required_params_absent.append("notification_name") + + if not description: + required_params_absent.append("description") + + subs_endpoints = syslog_notification_params.get('subscriptionEndpoints') + + if not subs_endpoints: + required_params_absent.append("syslog_dest_name") + + filters = syslog_notification_params.get("filter") + + if not filters.get("eventIds"): + required_params_absent.append("events_name_list") + + if required_params_absent: + self.status = "failed" + self.msg = """Missing required parameter '{0}' for adding Syslog Event Notification with given + name {1}""".format(str(required_params_absent), notification_name) + self.log(self.msg, "ERROR") + self.check_return_status() + + return self + + def create_syslog_notification(self, syslog_notification_params): + """ + Creates a Syslog Event Notification subscription in Cisco Catalyst Center based on the provided parameters. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + syslog_notification_params (list): A list containing a dictionary having the required parameter for creating + syslog event subscription notification. + Returns: + self (object): An instance of a class representing the status of the operation, including whether it was + successful or failed, any error messages encountered during operation. + Description: + This function makes an API call to create a Syslog Event Notification subscription in Cisco Catalyst Center. + It takes the provided parameters as input and constructs the payload for the API call. After making the + API call, it checks the status of the execution and updates the status and result attributes accordingly. + If the creation is successful, it sets the status to "success" and updates the result attribute with the + success message. If an error occurs during the process, it sets the status to "failed" and logs the + appropriate error message. + """ + + try: + notification_name = syslog_notification_params[0].get('name') + self.log("Requested payload for create_syslog_event_subscription - {0}".format(str(syslog_notification_params)), "INFO") + response = self.dnac._exec( + family="event_management", + function='create_syslog_event_subscription', + op_modifies=True, + params={'payload': syslog_notification_params} + ) + time.sleep(1) + self.log("Received API response from 'create_syslog_event_subscription': {0}".format(str(response)), "DEBUG") + status = response.get('statusUri') + status_execution_id = status.split("/")[-1] + status_response = self.check_status_api_events(status_execution_id) + + if status_response['apiStatus'] == "SUCCESS": + self.status = "success" + self.result['changed'] = True + self.msg = "Syslog Event Notification '{0}' created successfully in Cisco Catalyst Center".format(notification_name) + self.log(self.msg, "INFO") + self.result['response'] = self.msg + return self + + self.status = "failed" + error_messages = status_response.get('errorMessage') + + if error_messages: + failure_msg = error_messages.get('errors') + else: + failure_msg = "Unable to add Syslog Event Notification '{0}' in Cisco Catalyst Center.".format(notification_name) + + self.log(failure_msg, "ERROR") + self.result['response'] = failure_msg + + except Exception as e: + self.status = "failed" + self.msg = """Error while Adding the Syslog Event Subscription Notification with name '{0}' in Cisco Catalyst Center: + {1}""".format(notification_name, str(e)) + self.log(self.msg, "ERROR") + + return self + + def is_element_missing(self, playbook_list, ccc_list): + """ + Checks if any element in the playbook list is missing in the CCC list. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + playbook_list (list): List of elements from the playbook. + ccc_list (list): List of elements from the CCC. + Returns: + bool: True if any element from the playbook list is missing in the CCC list, False otherwise. + Description: + This function iterates through each element in the playbook list and checks if it is present in the CCC list. + If any element from the playbook list is not found in the CCC list, it returns True indicating that an element + is missing. If all elements are found, it returns False indicating that no element is missing. + """ + + for item in playbook_list: + if item not in ccc_list: + return True + + return False + + def compare_notification_filters(self, filters_in_playbook, filters_in_ccc): + """ + Compares notification filters between the playbook and Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + filters_in_playbook (dict): Dictionary containing notification filters from the playbook. + filters_in_ccc (dict): Dictionary containing notification filters from Cisco Catalyst Center. + Returns: + bool: True if notification filters need update, False otherwise. + Description: + This function compares notification filters between the playbook and Cisco Catalyst Center. + It iterates through each key-value pair in the playbook filters and checks if they match with the + corresponding key-value pair in the CCC filters. + If any mismatch is found, it logs a message indicating the need for an update and returns True. + If all filters match, it returns False indicating that no update is required. + """ + + for key, value in filters_in_playbook.items(): + if key == "domainsSubdomains": + domain_input = filters_in_playbook.get("domainsSubdomains")[0].get("domain") + subdomains_input = filters_in_playbook.get("domainsSubdomains")[0].get("subDomains") + domain_subdomain_in_ccc = filters_in_ccc.get("domainsSubdomains") + + if not domain_subdomain_in_ccc: + self.log("Since no domain subDomains present in Catalyst Center so notification needs update.", "INFO") + return True + domain_in_ccc = domain_subdomain_in_ccc.get("domain") + subdomain_in_ccc = domain_subdomain_in_ccc.get("subDomains") + + if domain_input and domain_input != domain_in_ccc: + self.log("Domain '{0}' given in the playbook doesnot match with domain in Cisco Catalyst Center".format(domain_input), "INFO") + return True + + if subdomains_input: + list_needs_update = self.is_element_missing(subdomains_input, subdomain_in_ccc) + if list_needs_update: + self.log("""Given sub_domains '{0}' in the playbook doesnot match with the value present in Cisco Catalyst Center + so notification needs update.""".format(subdomains_input), "INFO") + return True + elif isinstance(value, list): + list_needs_update = self.is_element_missing(value, filters_in_ccc[key]) + if list_needs_update: + self.log("""Parameter '{0}' given in the playbook doesnot match with the value present in Cisco Catalyst Center + so notification needs update.""".format(key), "INFO") + return True + + return False + + def syslog_notification_needs_update(self, syslog_notification_params, syslog_notification_in_ccc): + """ + Checks if a syslog notification needs update based on a comparison between playbook and CCC configurations. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + syslog_notification_params (dict): Dictionary containing syslog notification parameters from the playbook. + syslog_notification_in_ccc (dict): Dictionary containing syslog notification parameters from Cisco Catalyst Center. + Returns: + bool: True if the syslog notification needs update, False otherwise. + Description: + This function checks if a syslog notification needs update by comparing its parameters + with the corresponding parameters in Cisco Catalyst Center. + It compares the description, syslog destination, and filters between the playbook and CCC configurations. + If any parameter mismatch is found, it logs a message indicating the need for an update and returns True. + If all parameters match, it returns False indicating that no update is required. + """ + + syslog_notification_params = syslog_notification_params[0] + name = syslog_notification_params.get("name") + description_in_playbook = syslog_notification_params.get("description") + description_in_ccc = syslog_notification_in_ccc.get("description") + subs_endpoints = syslog_notification_params.get("subscriptionEndpoints") + ccc_endpoints = syslog_notification_in_ccc.get("subscriptionEndpoints")[0] + + if description_in_playbook and description_in_playbook != description_in_ccc: + self.log("""Parameter description doesnot match with the value of description present in Cisco Catalyst Center + so given Syslog Event Notification {0} needs an update""".format(name), "INFO") + return True + + if subs_endpoints: + instance_id = subs_endpoints[0].get("instanceId") + ccc_instance_id = ccc_endpoints.get("instanceId") + if instance_id != ccc_instance_id: + self.log("""Given Syslog destination in the playbook is different from syslog destination present in Cisco Catalyst Center + so given Syslog Event Notification {0} needs an update""".format(name), "INFO") + return True + filters_in_playbook = syslog_notification_params.get("filter") + filters_in_ccc = syslog_notification_in_ccc.get("filter") + + filters_need_update = self.compare_notification_filters(filters_in_playbook, filters_in_ccc) + + return filters_need_update + + def collect_notification_filter_params(self, playbook_params, filter, ccc_filter): + """ + Collects notification filter parameters from playbook and CCC configurations. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + playbook_params (dict): Dictionary containing parameters from the playbook. + filter (dict): Dictionary containing filter parameters from the playbook. + ccc_filter (dict): Dictionary containing filter parameters from Cisco Catalyst Center. + Returns: + dict: Dictionary containing updated playbook parameters with notification filter parameters. + Description: + This function collects notification filter parameters from both the playbook and CCC configurations. + It checks if filter parameters are provided in the playbook. If provided, it updates the playbook parameters + with the filter parameters from the playbook. If not provided, it updates the playbook parameters + with the filter parameters from Cisco Catalyst Center. + """ + + if filter: + playbook_params["filter"]["eventIds"] = filter.get("eventIds") or ccc_filter.get("eventIds") + playbook_params["filter"]["domainsSubdomains"] = filter.get("domainsSubdomains") or ccc_filter.get("domainsSubdomains") + playbook_params["filter"]["types"] = filter.get("types") or ccc_filter.get("types") + playbook_params["filter"]["categories"] = filter.get("categories") or ccc_filter.get("categories") + playbook_params["filter"]["severities"] = filter.get("severities") or ccc_filter.get("severities") + playbook_params["filter"]["sources"] = filter.get("sources") or ccc_filter.get("sources") + playbook_params["filter"]["siteIds"] = filter.get("siteIds") or ccc_filter.get("siteIds") + else: + # Need to take all required/optional parameter from Cisco Catalyst Center + playbook_params["filter"]["eventIds"] = ccc_filter.get("eventIds") + playbook_params["filter"]["domainsSubdomains"] = ccc_filter.get("domainsSubdomains") + playbook_params["filter"]["types"] = ccc_filter.get("types") + playbook_params["filter"]["categories"] = ccc_filter.get("categories") + playbook_params["filter"]["severities"] = ccc_filter.get("severities") + playbook_params["filter"]["sources"] = ccc_filter.get("sources") + playbook_params["filter"]["siteIds"] = ccc_filter.get("siteIds") + + return playbook_params + + def update_syslog_notification(self, syslog_notification_params, syslog_notification_in_ccc): + """ + Updates a Syslog Event Notification subscription in Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + syslog_notification_params (dict): Dictionary containing parameters for updating the Syslog Event Notification. + syslog_notification_in_ccc (dict): Dictionary containing current configuration of the Syslog Event Notification in CCC. + + Returns: + self (object): An instance of a class representing the status of the operation, including whether it was + successful or failed, any error messages encountered during operation. + Description: + This function updates a Syslog Event Notification subscription in Cisco Catalyst Center based on the provided parameters. + It constructs the payload for the update operation and sends it as an API request to the Cisco Catalyst Center. + After the update operation, it checks the status of the API request and logs appropriate messages based on the response. + """ + + syslog_notification_params = syslog_notification_params[0] + sys_notification_update_params = [] + name = syslog_notification_params.get("name") + description = syslog_notification_params.get("description") or syslog_notification_in_ccc.get("description") + version = syslog_notification_params.get("version") or syslog_notification_in_ccc.get("version") + subscription_id = syslog_notification_in_ccc.get("subscriptionId") + + playbook_params = { + "subscriptionId": subscription_id, + "name": name, + "description": description, + "version": version, + "filter": {} + } + subs_endpoints = syslog_notification_params.get("subscriptionEndpoints") + + if subs_endpoints: + playbook_params["subscriptionEndpoints"] = subs_endpoints + else: + playbook_params["subscriptionEndpoints"] = [] + instance_id = syslog_notification_in_ccc.get("subscriptionEndpoints")[0].get("instanceId") + temp_subscript_endpoint = { + "instanceId": instance_id, + "subscriptionDetails": { + "connectorType": "SYSLOG" + } + } + playbook_params['subscriptionEndpoints'].append(temp_subscript_endpoint) + + filter = syslog_notification_params.get("filter") + ccc_filter = syslog_notification_in_ccc.get("filter") + notification_params = self.collect_notification_filter_params(playbook_params, filter, ccc_filter) + sys_notification_update_params.append(notification_params) + + try: + self.log("Requested payload for update_syslog_event_subscription - {0}".format(str(sys_notification_update_params)), "INFO") + response = self.dnac._exec( + family="event_management", + function='update_syslog_event_subscription', + op_modifies=True, + params={'payload': sys_notification_update_params} + ) + time.sleep(1) + self.log("Received API response from 'update_syslog_event_subscription': {0}".format(str(response)), "DEBUG") + status = response.get('statusUri') + status_execution_id = status.split("/")[-1] + status_response = self.check_status_api_events(status_execution_id) + + if status_response['apiStatus'] == "SUCCESS": + self.status = "success" + self.result['changed'] = True + self.msg = "Syslog Event Notification '{0}' updated successfully in Cisco Catalyst Center".format(name) + self.log(self.msg, "INFO") + self.result['response'] = self.msg + return self + + self.status = "failed" + error_messages = status_response.get('errorMessage') + + if error_messages: + failure_msg = error_messages.get('errors') + else: + failure_msg = "Unable to update Syslog Event Notification '{0}' in Cisco Catalyst Center.".format(name) + + self.log(failure_msg, "ERROR") + self.result['response'] = failure_msg + + except Exception as e: + self.status = "failed" + self.msg = "Error while updating the Syslog Event Notification with name '{0}' in Cisco Catalyst Center: {1}".format(name, str(e)) + self.log(self.msg, "ERROR") + + return self + + def get_webhook_notification_details(self): + """ + Retrieves the details of a Webhook Event Notification subscription from the Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + notification_name (str): The name of the Webhook Event Notification subscription. + Returns: + dict or None: A dictionary containing the details of the Webhook Event Notification subscription if found. + Returns None if no subscription is found or if an error occurs during the API call. + Description: + This function calls an API to fetch the details of a specified Webhook Event Notification subscription. If the + subscription exists, it returns the response containing the subscription details. If no subscription is found + or an error occurs, it logs the appropriate message and handles the exception accordingly. + """ + + try: + response = self.dnac._exec( + family="event_management", + function='get_rest_webhook_event_subscriptions', + ) + self.log("Received API response from 'get_rest_webhook_event_subscriptions': {0}".format(str(response)), "DEBUG") + + if not response: + self.log("There is no Webhook Events Subscription Notification present in Cisco Catalyst Center.", "INFO") + return response + + return response + + except Exception as e: + self.status = "failed" + self.msg = """Error while getting the details of Webhook Event subscription notification present in + Cisco Catalyst Center: {0}""".format(str(e)) + self.log(self.msg, "ERROR") + self.check_return_status() + + def get_webhook_subscription_detail(self, webhook_dest_name): + """ + Retrieves the details of a specific webhook destination subscription from the Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + webhook_dest_name (str): The name of the webhook destination for which details needs to be fetched. + Returns: + dict or list: A dictionary containing the details of the webhook destination subscription if found. + Returns an empty list if no destination is found or if an error occurs during the API call. + Description: + This function calls an API to fetch the details of all webhook destination from the Cisco Catalyst Center. + It then searches for a subscription that matches the given `webhook_dest_name`. If a match is found, it returns + details of the matching subscription. If no match is found or if an error occurs, it logs the appropriate message + and handles the exception accordingly. + """ + + try: + response = self.dnac._exec( + family="event_management", + function='get_rest_webhook_subscription_details', + ) + self.log("Received API response from 'get_rest_webhook_subscription_details': {0}".format(str(response)), "DEBUG") + web_destination_details = [] + + if not response: + self.log("There is no webhook destination present in Cisco Catalyst Center.", "INFO") + return web_destination_details + + for dest in response: + if dest["name"] == webhook_dest_name: + return dest + self.log("There is no webhook destination with given name '{0}' present in Cisco Catalyst Center.".format(webhook_dest_name), "INFO") + + return web_destination_details + + except Exception as e: + self.status = "failed" + self.msg = """Error while getting the details of webhook Subscription with given name '{0}' present in + Cisco Catalyst Center: {1}""".format(webhook_dest_name, str(e)) + self.log(self.msg, "ERROR") + self.check_return_status() + + def collect_webhook_notification_playbook_params(self, webhook_notification_details): + """ + Collects and prepares parameters for creating or updating a webhook Event Notification. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + webhook_notification_details (dict): A dictionary containing the details required for creating or updating + the webhook Event Notification. + Returns: + list of dict: A list containing a dictionary with the parameters for creating the webhook Event Notification. + Description: + This function collects and structures the necessary parameters for creating or updating a webhook Event Notification. + It fetches additional details such as instance IDs and connector types from the Cisco Catalyst Center + and prepares the subscription endpoints and filters. The function handles missing or incorrect details by logging + appropriate messages and adjusting the status and returns a list containing required parameter. + """ + + webhook_notification_params = [] + name = webhook_notification_details.get('notification_name') + playbook_params = { + 'name': name, + 'description': webhook_notification_details.get('description'), + 'version': webhook_notification_details.get('version'), + 'subscriptionEndpoints': [], + 'filter': {} + + } + # Collect the Instance ID of the webhook destination + webhook_dest_name = webhook_notification_details.get('webhook_dest_name') + + if webhook_dest_name: + subscription_details = self.get_webhook_subscription_detail(webhook_dest_name) + + if not subscription_details: + self.status = "failed" + self.msg = """Unable to create/update the webhook event notification '{0}' as webhook desination '{1}' is not configured or + present in Cisco Catalyst Center""".format(name, webhook_dest_name) + self.log(self.msg, "ERROR") + self.check_return_status() + + instance_id = subscription_details.get('instanceId') + connector_type = subscription_details.get('connectorType') + temp_subscript_endpoint = { + "instanceId": instance_id, + "subscriptionDetails": { + "connectorType": connector_type + } + } + playbook_params["subscriptionEndpoints"].append(temp_subscript_endpoint) + + events_name_list = webhook_notification_details.get('events_name_list') + if events_name_list: + events_ids = self.get_event_ids(events_name_list) + + if not events_ids: + self.status = "failed" + self.msg = "Unable to create/update webhook event notification as given events name '{0}' are incorrect.".format(str(events_name_list)) + self.log(self.msg, "ERROR") + self.check_return_status() + + playbook_params["filter"]["eventIds"] = events_ids + + domain = webhook_notification_details.get("domain") + sub_domains = webhook_notification_details.get("sub_domains") + if domain and sub_domains: + playbook_params["filter"]["domainsSubdomains"] = [] + domain_dict = { + "domain": domain, + "subDomains": sub_domains + } + playbook_params["filter"]["domainsSubdomains"].append(domain_dict) + + event_types = webhook_notification_details.get("event_types") + if event_types: + playbook_params["filter"]["types"] = event_types + + event_categories = webhook_notification_details.get("event_categories") + if event_categories: + playbook_params["filter"]["categories"] = event_categories + + event_severities = webhook_notification_details.get("event_severities") + if event_severities: + playbook_params["filter"]["severities"] = event_severities + + event_sources = webhook_notification_details.get("event_sources") + if event_sources: + playbook_params["filter"]["sources"] = event_sources + + site_name_list = webhook_notification_details.get("site_name_list") + if site_name_list: + site_ids = self.get_site_ids(site_name_list) + + if not site_ids: + self.msg = "Unable to find the Site ID's for the given site(s) - '{0}' in the playbook's input.".format(str(site_name_list)) + self.log(self.msg, "INFO") + + playbook_params["filter"]["siteIds"] = site_ids + webhook_notification_params.append(playbook_params) + + return webhook_notification_params + + def mandatory_webhook_notification_parameter_check(self, webhook_notification_params): + """ + Checks for the presence of mandatory parameters required for adding a webhook Event Notification. + Args: + webhook_notification_params (list of dict): A list containing a single dictionary with the parameters + for the webhook Event Notification. + Returns: + self: The instance of the class with updated status and message if any required parameter is missing. + Description: + This function verifies the presence of required parameters for creating or updating a webhook Event Notification. + If any required parameter is absent, it logs an error message, updates the status to "failed", + and sets the message attribute. It then returns the instance of the class with the updated status and message. + """ + + required_params_absent = [] + webhook_notification_params = webhook_notification_params[0] + notification_name = webhook_notification_params.get("name") + description = webhook_notification_params.get("description") + + if not notification_name: + required_params_absent.append("notification_name") + + if not description: + required_params_absent.append("description") + + subs_endpoints = webhook_notification_params.get('subscriptionEndpoints') + + if not subs_endpoints: + required_params_absent.append("webhook_dest_name") + + filters = webhook_notification_params.get("filter") + + if not filters.get("eventIds"): + required_params_absent.append("events_name_list") + + if required_params_absent: + self.status = "failed" + self.msg = """Missing required parameter '{0}' for adding webhook Event Notification with given + name {1}""".format(str(required_params_absent), notification_name) + self.log(self.msg, "ERROR") + self.check_return_status() + + return self + + def create_webhook_notification(self, webhook_notification_params): + """ + Creates a webhook Event Notification subscription in Cisco Catalyst Center based on the provided parameters. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + webhook_notification_params (list): A list containing a dictionary having the required parameter for creating + webhook event subscription notification. + Returns: + self (object): An instance of a class representing the status of the operation, including whether it was + successful or failed, any error messages encountered during operation. + Description: + This function makes an API call to create a webhook Event Notification subscription in Cisco Catalyst Center. + It takes the provided parameters as input and constructs the payload for the API call. After making the + API call, it checks the status of the execution and updates the status and result attributes accordingly. + If the creation is successful, it sets the status to "success" and updates the result attribute with the + success message. If an error occurs during the process, it sets the status to "failed" and logs the + appropriate error message. + """ + + try: + notification_name = webhook_notification_params[0].get('name') + self.log("Requested payload for create_rest_webhook_event_subscription - {0}".format(str(webhook_notification_params)), "INFO") + response = self.dnac._exec( + family="event_management", + function='create_rest_webhook_event_subscription', + op_modifies=True, + params={'payload': webhook_notification_params} + ) + time.sleep(1) + self.log("Received API response from 'create_rest_webhook_event_subscription': {0}".format(str(response)), "DEBUG") + status = response.get('statusUri') + status_execution_id = status.split("/")[-1] + status_response = self.check_status_api_events(status_execution_id) + + if status_response['apiStatus'] == "SUCCESS": + self.status = "success" + self.result['changed'] = True + self.msg = "Webhook Event Subscription Notification '{0}' created successfully in Cisco Catalyst Center".format(notification_name) + self.log(self.msg, "INFO") + self.result['response'] = self.msg + return self + + self.status = "failed" + error_messages = status_response.get('errorMessage') + + if error_messages: + failure_msg = error_messages.get('errors') + else: + failure_msg = "Unable to add Webhook Events Subscription Notification '{0}' in Cisco Catalyst Center.".format(notification_name) + + self.log(failure_msg, "ERROR") + self.result['response'] = failure_msg + + except Exception as e: + self.status = "failed" + self.msg = "Error while Adding the webhook Event Notification with name '{0}' in Cisco Catalyst Center: {1}".format(notification_name, str(e)) + self.log(self.msg, "ERROR") + + return self + + def webhook_notification_needs_update(self, webhook_notification_params, webhook_notification_in_ccc): + """ + Checks if a webhook notification needs update based on a comparison between playbook and CCC configurations. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + webhook_notification_params (dict): Dictionary containing webhook notification parameters from the playbook. + webhook_notification_in_ccc (dict): Dictionary containing webhook notification parameters from Cisco Catalyst Center. + Returns: + bool: True if the webhook notification needs update, False otherwise. + Description: + This function checks if a webhook notification needs update by comparing its parameters + with the corresponding parameters in Cisco Catalyst Center. + It compares the description, webhook destination, and filters between the playbook and CCC configurations. + If any parameter mismatch is found, it logs a message indicating the need for an update and returns True. + If all parameters match, it returns False indicating that no update is required. + """ + + webhook_notification_params = webhook_notification_params[0] + name = webhook_notification_params.get("name") + description_in_playbook = webhook_notification_params.get("description") + description_in_ccc = webhook_notification_in_ccc.get("description") + subs_endpoints = webhook_notification_params.get("subscriptionEndpoints") + ccc_endpoints = webhook_notification_in_ccc.get("subscriptionEndpoints")[0] + + if description_in_playbook and description_in_playbook != description_in_ccc: + self.log("""Parameter description doesnot match with the value of description present in Cisco Catalyst Center + so given webhook Event Subscription Notification {0} needs an update""".format(name), "INFO") + return True + + if subs_endpoints: + instance_id = subs_endpoints[0].get("instanceId") + ccc_instance_id = ccc_endpoints.get("instanceId") + if instance_id != ccc_instance_id: + self.log("""Given webhook destination in the playbook is different from webhook destination present in Cisco Catalyst Center + so given webhook Event Subscription Notification {0} needs an update""".format(name), "INFO") + return True + filters_in_playbook = webhook_notification_params.get("filter") + filters_in_ccc = webhook_notification_in_ccc.get("filter") + + filters_need_update = self.compare_notification_filters(filters_in_playbook, filters_in_ccc) + + return filters_need_update + + def update_webhook_notification(self, webhook_notification_params, webhook_notification_in_ccc): + """ + Updates a Webhook Event Notification subscription in Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + webhook_notification_params (dict): Dictionary containing parameters for updating the webhook Event Notification. + webhook_notification_in_ccc (dict): Dictionary containing current configuration of the webhook Event Notification in CCC. + Returns: + self (object): An instance of a class representing the status of the operation, including whether it was + successful or failed, any error messages encountered during operation. + Description: + This function updates a Webhook Event Notification subscription in Cisco Catalyst Center based on the provided parameters. + It constructs the payload for the update operation and sends it as an API request to the Cisco Catalyst Center. + After the update operation, it checks the status of the API request and logs appropriate messages based on the response. + """ + + webhook_notification_params = webhook_notification_params[0] + web_notification_update_params = [] + name = webhook_notification_params.get("name") + description = webhook_notification_params.get("description") or webhook_notification_in_ccc.get("description") + version = webhook_notification_params.get("version") or webhook_notification_in_ccc.get("version") + subscription_id = webhook_notification_in_ccc.get("subscriptionId") + + playbook_params = { + "subscriptionId": subscription_id, + "name": name, + "description": description, + "version": version, + "filter": {} + } + subs_endpoints = webhook_notification_params.get("subscriptionEndpoints") + + if subs_endpoints: + playbook_params["subscriptionEndpoints"] = subs_endpoints + else: + playbook_params["subscriptionEndpoints"] = [] + instance_id = webhook_notification_in_ccc.get("subscriptionEndpoints")[0].get("instanceId") + temp_subscript_endpoint = { + "instanceId": instance_id, + "subscriptionDetails": { + "connectorType": "REST" + } + } + playbook_params['subscriptionEndpoints'].append(temp_subscript_endpoint) + + filter = webhook_notification_params.get("filter") + ccc_filter = webhook_notification_in_ccc.get("filter") + webhook_update_params = self.collect_notification_filter_params(playbook_params, filter, ccc_filter) + web_notification_update_params.append(webhook_update_params) + + try: + self.log("Requested payload for update_rest_webhook_event_subscription - {0}".format(str(web_notification_update_params)), "INFO") + response = self.dnac._exec( + family="event_management", + function='update_rest_webhook_event_subscription', + op_modifies=True, + params={'payload': web_notification_update_params} + ) + time.sleep(1) + self.log("Received API response from 'update_rest_webhook_event_subscription': {0}".format(str(response)), "DEBUG") + status = response.get('statusUri') + status_execution_id = status.split("/")[-1] + status_response = self.check_status_api_events(status_execution_id) + + if status_response['apiStatus'] == "SUCCESS": + self.status = "success" + self.result['changed'] = True + self.msg = "Webhook Event Subscription Notification '{0}' updated successfully in Cisco Catalyst Center".format(name) + self.log(self.msg, "INFO") + self.result['response'] = self.msg + return self + + self.status = "failed" + error_messages = status_response.get('errorMessage') + + if error_messages: + failure_msg = error_messages.get('errors') + else: + failure_msg = "Unable to update webhook Event Subscription Notification '{0}' in Cisco Catalyst Center.".format(name) + + self.log(failure_msg, "ERROR") + self.result['response'] = failure_msg + + except Exception as e: + self.status = "failed" + self.msg = """Error while updating the Webhook Event Subscription Notification with name '{0}' in Cisco Catalyst + Center: {1}""".format(name, str(e)) + self.log(self.msg, "ERROR") + + return self + + def get_email_notification_details(self): + """ + Retrieves the details of a email Event Notification subscription from the Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + notification_name (str): The name of the email Event Notification subscription/ + Returns: + dict or None: A dictionary containing the details of the email Event Notification subscription if found. + Returns None if no subscription is found or if an error occurs during the API call. + Description: + This function calls an API to fetch the details of a specified email Event Notification subscription. If the + subscription exists, it returns the response containing the subscription details. If no subscription is found + or an error occurs, it logs the appropriate message and handles the exception accordingly. + """ + + try: + response = self.dnac._exec( + family="event_management", + function='get_email_event_subscriptions', + ) + self.log("Received API response from 'get_email_event_subscriptions': {0}".format(str(response)), "DEBUG") + + if not response: + self.log("There is no Email Events Subscription Notification present in Cisco Catalyst Center.", "INFO") + return response + + return response + except Exception as e: + self.status = "failed" + self.msg = """Error while getting the details of Email Event subscription notification present in + Cisco Catalyst Center: {0}""".format(str(e)) + self.log(self.msg, "ERROR") + self.check_return_status() + + def get_email_subscription_detail(self, instance_name): + """ + Retrieves the details of a specific email destination subscription from the Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + instance_name (str): The name of the email destination for which details needs to be fetched. + Returns: + dict or list: A dictionary containing the details of the email destination subscription if found. + Returns an empty list if no destination is found or if an error occurs during the API call. + Description: + This function calls an API to fetch the details of all email destination from the Cisco Catalyst Center. + It then searches for a subscription that matches the given `instance_name`. If a match is found, it returns + details of the matching subscription. If no match is found or if an error occurs, it logs the appropriate message + and handles the exception accordingly. + """ + + try: + response = self.dnac._exec( + family="event_management", + function='get_email_subscription_details', + ) + self.log("Received API response from 'get_email_subscription_details': {0}".format(str(response)), "DEBUG") + email_destination_details = None + + if not response: + self.log("There is no email destination present in Cisco Catalyst Center.", "INFO") + return email_destination_details + + for dest in response: + if dest["name"] == instance_name: + return dest + self.log("There is no email destination with given name '{0}' present in Cisco Catalyst Center.".format(instance_name), "INFO") + + return email_destination_details + + except Exception as e: + self.status = "failed" + self.msg = """Error while getting the details of Email event Subscription with given destination name '{0}' present in + Cisco Catalyst Center: {1}""".format(instance_name, str(e)) + self.log(self.msg, "ERROR") + self.check_return_status() + + def collect_email_notification_playbook_params(self, email_notification_details): + """ + Collects and prepares parameters for creating or updating a email Event Notification. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + email_notification_details (dict): A dictionary containing the details required for creating or updating + the email Event Notification. + Returns: + list of dict: A list containing a dictionary with the parameters for creating the email Event Notification. + Description: + This function collects and structures the necessary parameters for creating or updating a email Event Notification. + It fetches additional details such as instance IDs and connector types from the Cisco Catalyst Center + and prepares the subscription endpoints and filters. The function handles missing or incorrect details by logging + appropriate messages and adjusting the status and returns a list containing required parameter. + """ + + email_notification_params = [] + email_notf_name = email_notification_details.get('notification_name') + playbook_params = { + 'name': email_notf_name, + 'description': email_notification_details.get('description'), + 'version': email_notification_details.get('version'), + 'subscriptionEndpoints': [], + 'filter': {} + + } + # Collect the Instance ID of the email destination + instance_name = email_notification_details.get('instance_name') + + if not instance_name: + self.status = "failed" + self.msg = """Name parameter for the Subscription Endpoints is required for creating/updating Email Events Subscription + Notification with given name {0} in Cisco Catalyst Center""".format(email_notf_name) + self.log(self.msg, "ERROR") + return self + + subscription_details = self.get_email_subscription_detail(instance_name) + instance_id = None + fromEmailAddress = email_notification_details.get("from_email_address") + toEmailAddresses = email_notification_details.get("to_email_addresses") + subject = email_notification_details.get("subject") + description = email_notification_details.get("instance_description") + + if subscription_details: + instance_id = subscription_details.get("instanceId") + if not fromEmailAddress: + fromEmailAddress = subscription_details.get("fromEmailAddress") + if not toEmailAddresses: + toEmailAddresses = subscription_details.get("toEmailAddresses") + if not subject: + subject = subscription_details.get("subject") + if not description: + description = subscription_details.get("description") + + temp_subscript_endpoint = { + "instanceId": instance_id, + "subscriptionDetails": { + "connectorType": "EMAIL", + "fromEmailAddress": fromEmailAddress, + "toEmailAddresses": toEmailAddresses, + "subject": subject, + "name": instance_name, + "description": description + } + } + playbook_params["subscriptionEndpoints"].append(temp_subscript_endpoint) + + events_name_list = email_notification_details.get('events_name_list') + if events_name_list: + events_ids = self.get_event_ids(events_name_list) + + if not events_ids: + self.status = "failed" + self.msg = "Unable to create/update email event notification as given events name '{0}' are incorrect.".format(str(events_name_list)) + self.log(self.msg, "ERROR") + self.check_return_status() + + playbook_params["filter"]["eventIds"] = events_ids + + domain = email_notification_details.get("domain") + sub_domains = email_notification_details.get("sub_domains") + if domain and sub_domains: + playbook_params["filter"]["domainsSubdomains"] = [] + domain_dict = { + "domain": domain, + "subDomains": sub_domains + } + playbook_params["filter"]["domainsSubdomains"].append(domain_dict) + + event_types = email_notification_details.get("event_types") + if event_types: + playbook_params["filter"]["types"] = event_types + + event_categories = email_notification_details.get("event_categories") + if event_categories: + playbook_params["filter"]["categories"] = event_categories + + event_severities = email_notification_details.get("event_severities") + if event_severities: + playbook_params["filter"]["severities"] = event_severities + + event_sources = email_notification_details.get("event_sources") + if event_sources: + playbook_params["filter"]["sources"] = event_sources + + site_name_list = email_notification_details.get("site_name_list") + if site_name_list: + site_ids = self.get_site_ids(site_name_list) + + if not site_ids: + self.msg = "Unable to find the Site ID's for the given site(s) - '{0}' in the playbook's input.".format(str(site_name_list)) + self.log(self.msg, "INFO") + + playbook_params["filter"]["siteIds"] = site_ids + email_notification_params.append(playbook_params) + + return email_notification_params + + def mandatory_email_notification_parameter_check(self, email_notification_params): + """ + Checks for the presence of mandatory parameters required for adding a Email Event Subscription Notification. + Args: + email_notification_params (list of dict): A list containing a single dictionary with the parameters + for the email Event Notification. + Returns: + self: The instance of the class with updated status and message if any required parameter is missing. + Description: + This function verifies the presence of required parameters for creating or updating a email Event Notification. + If any required parameter is absent, it logs an error message, updates the status to "failed", + and sets the message attribute. It then returns the instance of the class with the updated status and message. + """ + + required_params_absent = [] + email_notification_params = email_notification_params[0] + notification_name = email_notification_params.get("name") + description = email_notification_params.get("description") + + if not notification_name: + required_params_absent.append("notification_name") + + if not description: + required_params_absent.append("description") + + subs_endpoints = email_notification_params.get('subscriptionEndpoints') + + if not subs_endpoints: + required_params_absent.extends(["instance_name", "fromEmailAddress", "to_email_addresses", "subject"]) + else: + subs_endpoints = subs_endpoints[0].get("subscriptionDetails") + if not subs_endpoints.get("fromEmailAddress"): + required_params_absent.append("from_email_address") + if not subs_endpoints.get("toEmailAddresses"): + required_params_absent.append("to_email_addresses") + if not subs_endpoints.get("subject"): + required_params_absent.append("subject") + if not subs_endpoints.get("name"): + required_params_absent.append("instance_name") + + filters = email_notification_params.get("filter") + + if not filters.get("eventIds"): + required_params_absent.append("events_name_list") + + if required_params_absent: + self.status = "failed" + self.msg = """Missing required parameter '{0}' for adding Email Events Subscription Notification + with given name {1}""".format(str(required_params_absent), notification_name) + self.log(self.msg, "ERROR") + self.check_return_status() + + return self + + def create_email_notification(self, email_notification_params): + """ + Creates a Email Event Notification Subscription in Cisco Catalyst Center based on the provided parameters. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + email_notification_params (list): A list containing a dictionary having the required parameter for creating + email event subscription notification. + Returns: + self (object): An instance of a class representing the status of the operation, including whether it was + successful or failed, any error messages encountered during operation. + Description: + This function makes an API call to create a Email Event Notification subscription in Cisco Catalyst Center. + It takes the provided parameters as input and constructs the payload for the API call. After making the + API call, it checks the status of the execution and updates the status and result attributes accordingly. + If the creation is successful, it sets the status to "success" and updates the result attribute with the + success message. If an error occurs during the process, it sets the status to "failed" and logs the + appropriate error message. + """ + + try: + notification_name = email_notification_params[0].get('name') + self.log("Requested payload for create_email_event_subscription - {0}".format(str(email_notification_params)), "INFO") + response = self.dnac._exec( + family="event_management", + function='create_email_event_subscription', + op_modifies=True, + params={'payload': email_notification_params} + ) + time.sleep(1) + self.log("Received API response from 'create_email_event_subscription': {0}".format(str(response)), "DEBUG") + status = response.get('statusUri') + status_execution_id = status.split("/")[-1] + status_response = self.check_status_api_events(status_execution_id) + + if status_response['apiStatus'] == "SUCCESS": + self.status = "success" + self.result['changed'] = True + self.msg = "Email Event Subscription Notification '{0}' created successfully in Cisco Catalyst Center".format(notification_name) + self.log(self.msg, "INFO") + self.result['response'] = self.msg + return self + + self.status = "failed" + error_messages = status_response.get('errorMessage') + + if error_messages: + failure_msg = error_messages.get('errors') + else: + failure_msg = "Unable to add Email Events Subscription Notification '{0}' in Cisco Catalyst Center.".format(notification_name) + + self.log(failure_msg, "ERROR") + self.result['response'] = failure_msg + + except Exception as e: + self.status = "failed" + self.msg = "Error while Adding the email Event Notification with name '{0}' in Cisco Catalyst Center: {1}".format(notification_name, str(e)) + self.log(self.msg, "ERROR") + + return self + + def compare_email_subs_endpoints(self, subs_endpoints, ccc_endpoints): + """ + Compare email subscription endpoints parameters to determine if they match or not. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + subs_endpoints (dict): A dictionary containing the subscription endpoint parameters from the playbook. + ccc_endpoints (dict): A dictionary containing the current subscription endpoint parameters in Cisco Catalyst Center. + Returns: + bool: Returns True if there is any difference between the parameters in subs_endpoints and ccc_endpoints, otherwise False. + Description: + This function compares the specified parameters of email subscription endpoints from the provided dictionaries. + If any of the parameters differ between subs_endpoints and ccc_endpoints, the function returns True, indicating + that the subscription endpoints need to be updated. If all parameters match, the function returns False. + """ + + params_to_compare = ["fromEmailAddress", "toEmailAddresses", "subject", "name", "description"] + subs_endpoints = subs_endpoints.get("subscriptionDetails") + ccc_endpoints = ccc_endpoints.get("subscriptionDetails") + + for param in params_to_compare: + playbook_param = subs_endpoints.get(param) + if isinstance(playbook_param, list): + ccc_list_param = ccc_endpoints.get(param) + list_needs_update = self.is_element_missing(playbook_param, ccc_list_param) + + if list_needs_update: + self.log("""Parameter '{0}' given in the playbook doesnot match with the value present in Cisco Catalyst Center + so notification needs update.""".format(param), "INFO") + return True + elif subs_endpoints.get(param) != ccc_endpoints.get(param): + return True + + return False + + def email_notification_needs_update(self, email_notification_params, email_notification_in_ccc): + """ + Checks if a Email notification needs update based on a comparison between playbook and CCC configurations. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + email_notification_params (dict): Dictionary containing email notification parameters from the playbook. + email_notification_in_ccc (dict): Dictionary containing email notification parameters from Cisco Catalyst Center. + Returns: + bool: True if the email notification needs update, False otherwise. + Description: + This function checks if a email notification needs update by comparing its parameters + with the corresponding parameters in Cisco Catalyst Center. + It compares the description, email destination, and filters between the playbook and CCC configurations. + If any parameter mismatch is found, it logs a message indicating the need for an update and returns True. + If all parameters match, it returns False indicating that no update is required. + """ + + email_notification_params = email_notification_params[0] + name = email_notification_params.get("name") + description_in_playbook = email_notification_params.get("description") + description_in_ccc = email_notification_in_ccc.get("description") + subs_endpoints = email_notification_params.get("subscriptionEndpoints") + ccc_endpoints = email_notification_in_ccc.get("subscriptionEndpoints")[0] + + if description_in_playbook and description_in_playbook != description_in_ccc: + self.log("""Parameter description doesnot match with the value of description present in Cisco Catalyst Center + so given email Event Subscription Notification {0} needs an update""".format(name), "INFO") + return True + + if subs_endpoints: + subs_endpoints = subs_endpoints[0] + notification_need_update = self.compare_email_subs_endpoints(subs_endpoints, ccc_endpoints) + + if notification_need_update: + self.log("""Given Email destination in the playbook is different from email destination present in Cisco Catalyst Center + so given email Event Subscription Notification {0} needs an update""".format(name), "INFO") + return True + + filters_in_playbook = email_notification_params.get("filter") + filters_in_ccc = email_notification_in_ccc.get("filter") + filters_need_update = self.compare_notification_filters(filters_in_playbook, filters_in_ccc) + + return filters_need_update + + def update_email_notification(self, email_notification_params, email_notification_in_ccc): + """ + Updates a Email Event Notification subscription in Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + email_notification_params (dict): Dictionary containing parameters for updating the Email Event Notification. + email_notification_in_ccc (dict): Dictionary containing current configuration of the Email Event Notification in CCC. + Returns: + self (object): An instance of a class representing the status of the operation, including whether it was + successful or failed, any error messages encountered during operation. + Description: + This function updates a email Event Notification subscription in Cisco Catalyst Center based on the provided parameters. + It constructs the payload for the update operation and sends it as an API request to the Cisco Catalyst Center. + After the update operation, it checks the status of the API request and logs appropriate messages based on the response. + """ + + email_notification_params = email_notification_params[0] + email_notification_update_params = [] + name = email_notification_params.get("name") + description = email_notification_params.get("description") or email_notification_in_ccc.get("description") + version = email_notification_params.get("version") or email_notification_in_ccc.get("version") + subscription_id = email_notification_in_ccc.get("subscriptionId") + + playbook_params = { + "subscriptionId": subscription_id, + "name": name, + "description": description, + "version": version, + "filter": {}, + "subscriptionEndpoints": [] + } + subs_endpoints = email_notification_params.get("subscriptionEndpoints") + subs_endpoints_in_ccc = email_notification_in_ccc.get("subscriptionEndpoints")[0] + instance_id = subs_endpoints_in_ccc.get("instanceId") + + if subs_endpoints: + playbook_params["subscriptionEndpoints"] = subs_endpoints + else: + playbook_params["subscriptionEndpoints"] = [] + fromEmailAddress = subs_endpoints_in_ccc.get('fromEmailAddress') + toEmailAddresses = subs_endpoints_in_ccc.get('toEmailAddresses') + subject = subs_endpoints_in_ccc.get('subject') + instance_name = subs_endpoints_in_ccc.get('name') + instance_description = subs_endpoints_in_ccc.get('description') + + temp_subscript_endpoint = { + "instanceId": instance_id, + "subscriptionDetails": { + "connectorType": "EMAIL" + }, + "fromEmailAddress": fromEmailAddress, + "toEmailAddresses": toEmailAddresses, + "subject": subject, + "name": instance_name, + "description": instance_description + + } + playbook_params['subscriptionEndpoints'].append(temp_subscript_endpoint) + + filter = email_notification_params.get("filter") + ccc_filter = email_notification_in_ccc.get("filter") + email_update_params = self.collect_notification_filter_params(playbook_params, filter, ccc_filter) + email_notification_update_params.append(email_update_params) + + try: + self.log("Requested payload for update_rest_email_event_subscription - {0}".format(str(email_notification_update_params)), "INFO") + response = self.dnac._exec( + family="event_management", + function='update_email_event_subscription', + op_modifies=True, + params={'payload': email_notification_update_params} + ) + time.sleep(2) + self.log("Received API response from 'update_email_event_subscription': {0}".format(str(response)), "DEBUG") + status = response.get('statusUri') + status_execution_id = status.split("/")[-1] + status_response = self.check_status_api_events(status_execution_id) + + if status_response['apiStatus'] == "SUCCESS": + self.status = "success" + self.result['changed'] = True + self.msg = "Email Event Subscription Notification '{0}' updated successfully in Cisco Catalyst Center".format(name) + self.log(self.msg, "INFO") + self.result['response'] = self.msg + return self + + self.status = "failed" + error_messages = status_response.get('errorMessage') + + if error_messages: + failure_msg = error_messages.get('errors') + else: + failure_msg = "Unable to update Email Event Subscription Notification '{0}' in Cisco Catalyst Center.".format(name) + + self.log(failure_msg, "ERROR") + self.result['response'] = failure_msg + self.msg = failure_msg + + except Exception as e: + self.status = "failed" + self.msg = """Error while updating the Email Event Subscription Notification with name '{0}' in Cisco Catalyst + Center: {1}""".format(name, str(e)) + self.log(self.msg, "ERROR") + + return self + + def delete_events_subscription_notification(self, subscription_id, subscription_name): + """ + Delete an event subscription notification from Cisco Catalyst Center. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + subscription_id (str): The ID of the subscription notification to be deleted. + subscription_name (str): The name of the subscription notification to be deleted. + Returns: + self (object): Returns the instance of the class with updated status and result. + Description: + This function deletes an event subscription notification from Cisco Catalyst Center using the provided + subscription ID for Webhook, Email and Syslog events subscription notification. If the deletion is successful, + it updates the status to 'success' and logs the success message. If the deletion fails, it updates the status + to 'failed' and logs the error message. + The function also calls 'check_status_api_events' to monitor the deletion status and ensure the process + is completed successfully before returning the result. + """ + + try: + response = self.dnac._exec( + family="event_management", + function="delete_event_subscriptions", + op_modifies=True, + params={"subscriptions": subscription_id}, + ) + self.log("Received API response from 'update_email_event_subscription': {0}".format(str(response)), "DEBUG") + status = response.get('statusUri') + status_execution_id = status.split("/")[-1] + status_response = self.check_status_api_events(status_execution_id) + + if status_response['apiStatus'] == "SUCCESS": + self.status = "success" + self.result['changed'] = True + self.msg = "Event Subscription Notification '{0}' deleted successfully from Cisco Catalyst Center".format(subscription_name) + self.log(self.msg, "INFO") + self.result['response'] = self.msg + return self + + self.status = "failed" + error_messages = status_response.get('errorMessage') + + if error_messages: + failure_msg = error_messages.get('errors') + else: + failure_msg = "Unable to delete Event Subscription Notification '{0}' from Cisco Catalyst Center.".format(subscription_name) + + self.log(failure_msg, "ERROR") + self.result['response'] = failure_msg + self.msg = failure_msg + except Exception as e: + self.status = "failed" + self.msg = "Exception occurred while deleting Event Subscription Notification '{0}' due to: {1}".format(subscription_name, str(e)) + self.log(self.msg, "ERROR") + + return self + + def get_diff_merged(self, config): + """ + Processes the configuration difference and merges them into Cisco Catalyst Center. + This method updates Cisco Catalyst Center configurations based on the differences detected + between the desired state (`want`) and the current state (`have`). It handles different + types of configurations such as syslog, SNMP, REST webhook, email, and ITSM settings. + Args: + self (object): An instance of a class used for interacting with Cisco Catalyst Center. + config (dict): A dictionary containing various destination settings that may include + syslog destination, SNMP destination, REST webhook destination, + email destination, and ITSM settings. Each key should point to a dictionary + that defines specific configuration for that setting type. + Return: + self (object): Returns the instance itself after potentially modifying internal state to reflect + the status of operation, messages to log, and response details. + Description: + This method acts as a controller that delegates specific tasks such as adding or updating + configurations for syslog, SNMP, REST webhook, email, and ITSM settings in the Cisco Catalyst + Center. It ensures required parameters are present, validates them, and calls the appropriate + methods to add or update the configurations. Error handling is included to manage any exceptions + or invalid configurations, updating the internal state to reflect these errors. + """ + + # Create/Update Rest Webhook destination in Cisco Catalyst Center + if config.get('webhook_destination'): + webhook_details = self.want.get('webhook_details') + destination_name = webhook_details.get('name') + + if not destination_name: + self.status = "failed" + self.msg = "Name is required parameter for adding/updating Webhook destination for creating/updating the event." + self.log(self.msg, "ERROR") + return self + + is_destination_exist = False + for webhook_dict in self.have.get('webhook_destinations'): + if webhook_dict['name'] == destination_name: + webhook_dest_detail_in_ccc = webhook_dict + is_destination_exist = True + break + webhook_params = self.collect_webhook_playbook_params(webhook_details) + + if webhook_params.get('method') not in ["POST", "PUT"]: + self.status = "failed" + self.msg = """Invalid Webhook method name '{0}' for creating/updating Webhook destination in Cisco Catalyst Center. + Select one of the following method 'POST/PUT'.""".format(webhook_params.get('method')) + self.log(self.msg, "ERROR") + return self + + regex_pattern = re.compile( + r'^https://' # Ensure the URL starts with "https://" r'(' r'(([A-Za-z0-9-*.&@]+\.)+[A-Za-z]{2,6})|' # Domain name with wildcards and special characters r'localhost|' # Localhost @@ -2501,6 +4508,130 @@ def get_diff_merged(self, config): # Update the ITSM integration settings with given details in the playbook self.update_itsm_integration_setting(itsm_params, itsm_in_ccc).check_return_status() + # Create Rest Webhook Events Subscription Notification in Cisco Catalyst Center + if config.get('webhook_event_notification'): + webhook_notification_details = self.want.get('webhook_event_notification') + notification_name = webhook_notification_details.get('notification_name') + + if not notification_name: + self.status = "failed" + self.msg = """Name is required parameter for creating/updating webhook events subscription notification + in Cisco Catalyst Center.""" + self.log(self.msg, "ERROR") + return self + + webhook_notification_params = self.collect_webhook_notification_playbook_params(webhook_notification_details) + current_webhook_notifications = self.have.get("webhook_subscription_notifications") + is_webhook_notification_exist = False + + if current_webhook_notifications: + for notification in current_webhook_notifications: + if notification["name"] == notification_name: + is_webhook_notification_exist = True + webhook_notification_in_ccc = notification + break + + if not is_webhook_notification_exist: + # Need to create webhook event notification in Cisco Catalyst Center + self.mandatory_webhook_notification_parameter_check(webhook_notification_params).check_return_status() + self.log("""Successfully validated the required parameter for creating the Webhook Event Notification with + given name '{0}'""".format(notification_name), "INFO") + self.create_webhook_notification(webhook_notification_params).check_return_status() + else: + # Check whether the webhook evenet notification needs any update or not. + webhook_notification_need_update = self.webhook_notification_needs_update(webhook_notification_params, webhook_notification_in_ccc) + if not webhook_notification_need_update: + self.msg = "Webhook Notification with name '{0}' needs no update in Cisco Catalyst Center".format(notification_name) + self.log(self.msg, "INFO") + self.result['changed'] = False + self.result['response'] = self.msg + else: + # Update the webhook notification with given playbook parameters + self.update_webhook_notification(webhook_notification_params, webhook_notification_in_ccc).check_return_status() + + # Create Email Events Subscription Notification in Cisco Catalyst Center + if config.get('email_event_notification'): + email_notification_details = self.want.get('email_event_notification') + notification_name = email_notification_details.get('notification_name') + + if not notification_name: + self.status = "failed" + self.msg = """Name is required parameter for creating/updating Email events subscription notification + in Cisco Catalyst Center.""" + self.log(self.msg, "ERROR") + return self + + email_notification_params = self.collect_email_notification_playbook_params(email_notification_details) + current_email_notifications = self.have.get("email_subscription_notifications") + is_email_notification_exist = False + + if current_email_notifications: + for notification in current_email_notifications: + if notification["name"] == notification_name: + is_email_notification_exist = True + email_notification_in_ccc = notification + break + + if not is_email_notification_exist: + # Need to create email event notification in Cisco Catalyst Center + self.mandatory_email_notification_parameter_check(email_notification_params).check_return_status() + self.log("""Successfully validated the required parameter for creating the email Event Notification with + given name '{0}'""".format(notification_name), "INFO") + self.create_email_notification(email_notification_params).check_return_status() + else: + # Check whether the email evenet notification needs any update or not. + email_notification_need_update = self.email_notification_needs_update(email_notification_params, email_notification_in_ccc) + + if not email_notification_need_update: + self.msg = "Email Notification with name '{0}' needs no update in Cisco Catalyst Center".format(notification_name) + self.log(self.msg, "INFO") + self.result['changed'] = False + self.result['response'] = self.msg + else: + # Update the email notification with given playbook parameters + self.update_email_notification(email_notification_params, email_notification_in_ccc).check_return_status() + + # Create Syslog Events Subscription Notification in Cisco Catalyst Center + if config.get('syslog_event_notification'): + syslog_notification_details = self.want.get('syslog_event_notification') + notification_name = syslog_notification_details.get('notification_name') + + if not notification_name: + self.status = "failed" + self.msg = """Name is required parameter for creating/updating syslog events subscription notification + in Cisco Catalyst Center.""" + self.log(self.msg, "ERROR") + return self + + syslog_notification_params = self.collect_syslog_notification_playbook_params(syslog_notification_details) + current_syslog_notifications = self.have.get("syslog_subscription_notifications") + is_syslog_notification_exist = False + + if current_syslog_notifications: + for notification in current_syslog_notifications: + if notification["name"] == notification_name: + is_syslog_notification_exist = True + syslog_notification_in_ccc = notification + break + + if not is_syslog_notification_exist: + # Need to create syslog event notification in Cisco Catalyst Center + self.mandatory_syslog_notification_parameter_check(syslog_notification_params).check_return_status() + self.log("""Successfully validated the required parameter for creating the Syslog Event Notification with + given name '{0}'""".format(notification_name), "INFO") + self.create_syslog_notification(syslog_notification_params).check_return_status() + else: + # Check whether the syslog evenet notification needs any update or not. + sys_notification_need_update = self.syslog_notification_needs_update(syslog_notification_params, syslog_notification_in_ccc) + if not sys_notification_need_update: + self.msg = "Syslog Notification with name '{0}' needs no update in Cisco Catalyst Center".format(notification_name) + self.log(self.msg, "INFO") + self.result['changed'] = False + self.result['response'] = self.msg + else: + # Update the syslog notification with given playbook parameters + self.update_syslog_notification(syslog_notification_params, syslog_notification_in_ccc).check_return_status() + return self def get_diff_deleted(self, config): @@ -2574,6 +4705,93 @@ def get_diff_deleted(self, config): self.result['changed'] = False self.result['response'] = self.msg + # Delete Webhook Events Subscription Notification from Cisco Catalyst Center + if config.get('webhook_event_notification'): + webhook_notification_details = self.want.get('webhook_event_notification') + webhook_notification_name = webhook_notification_details.get('notification_name') + current_webhook_notifications = self.have.get("webhook_subscription_notifications") + webhook_notification_id = None + + if not current_webhook_notifications: + self.status = "success" + self.result['changed'] = False + self.msg = """There is no Webhook Event Subscription Notification with name '{0}' present in Cisco Catalyst Center + so cannot delete the notification.""".format(webhook_notification_name) + self.log(self.name, "INFO") + return self + + for notification in current_webhook_notifications: + if notification["name"] == webhook_notification_name: + webhook_notification_id = notification["subscriptionId"] + break + + if webhook_notification_id: + self.delete_events_subscription_notification(webhook_notification_id, webhook_notification_name).check_return_status() + else: + self.msg = """Unable to delete Webhook Event Subscription Notification with name '{0}' as it is not present in Cisco + Catalyst Center""".format(webhook_notification_name) + self.log(self.msg, "INFO") + self.result['changed'] = False + self.result['response'] = self.msg + + # Delete Email Events Subscription Notification from Cisco Catalyst Center + if config.get('email_event_notification'): + email_notification_details = self.want.get('email_event_notification') + email_notification_name = email_notification_details.get('notification_name') + current_email_notifications = self.have.get("email_subscription_notifications") + email_notification_id = None + + if not current_email_notifications: + self.status = "success" + self.result['changed'] = False + self.msg = """There is no email Event Subscription Notification with name '{0}' present in Cisco Catalyst Center + so cannot delete the notification.""".format(email_notification_name) + self.log(self.name, "INFO") + return self + + for notification in current_email_notifications: + if notification["name"] == email_notification_name: + email_notification_id = notification["subscriptionId"] + break + + if email_notification_id: + self.delete_events_subscription_notification(email_notification_id, email_notification_name).check_return_status() + else: + self.msg = """Unable to delete Email Event Subscription Notification with name '{0}' as it is not present in Cisco + Catalyst Center""".format(email_notification_name) + self.log(self.msg, "INFO") + self.result['changed'] = False + self.result['response'] = self.msg + + # Delete Syslog Events Subscription Notification from Cisco Catalyst Center + if config.get('syslog_event_notification'): + syslog_notification_details = self.want.get('syslog_event_notification') + syslog_notification_name = syslog_notification_details.get('notification_name') + current_syslog_notifications = self.have.get("syslog_subscription_notifications") + syslog_notification_id = None + + if not current_syslog_notifications: + self.status = "success" + self.result['changed'] = False + self.msg = """There is no Syslog Event Subscription Notification with name '{0}' present in Cisco Catalyst Center + so cannot delete the notification.""".format(syslog_notification_name) + self.log(self.name, "INFO") + return self + + for notification in current_syslog_notifications: + if notification["name"] == syslog_notification_name: + syslog_notification_id = notification["subscriptionId"] + break + + if syslog_notification_id: + self.delete_events_subscription_notification(syslog_notification_id, syslog_notification_name).check_return_status() + else: + self.msg = """Unable to delete Syslog Event Subscription Notification with name '{0}' as it is not present in Cisco + Catalyst Center""".format(syslog_notification_name) + self.log(self.msg, "INFO") + self.result['changed'] = False + self.result['response'] = self.msg + return self def verify_diff_merged(self, config): @@ -2686,6 +4904,69 @@ def verify_diff_merged(self, config): self.log("""Playbook's input does not match with Cisco Catalyst Center, indicating that ITSM Integration setting with name '{0}' addition/updation task may not have executed successfully.""".format(itsm_name), "INFO") + if config.get('webhook_event_notification'): + webhook_notification_details = self.want.get('webhook_event_notification') + web_notification_name = webhook_notification_details.get('notification_name') + current_webhook_notifications = self.have.get("webhook_subscription_notifications") + is_webhook_notification_exist = False + + if current_webhook_notifications: + for notification in current_webhook_notifications: + if notification["name"] == web_notification_name: + is_webhook_notification_exist = True + break + + if is_webhook_notification_exist: + self.status = "success" + msg = """Requested Webhook Events Subscription Notification '{0}' have been successfully created/updated to the Cisco Catalyst Center + and their creation/updation has been verified.""".format(web_notification_name) + self.log(msg, "INFO") + else: + self.log("""Playbook's input does not match with Cisco Catalyst Center, indicating that Webhook Event Subscription Notification with + name '{0}' creation/updation task may not have executed successfully.""".format(web_notification_name), "INFO") + + if config.get('email_event_notification'): + email_notification_details = self.want.get('email_event_notification') + email_notification_name = email_notification_details.get('notification_name') + current_email_notifications = self.have.get("email_subscription_notifications") + is_email_notification_exist = False + + if current_email_notifications: + for notification in current_email_notifications: + if notification["name"] == email_notification_name: + is_email_notification_exist = True + break + + if is_email_notification_exist: + self.status = "success" + msg = """Requested Email Events Subscription Notification '{0}' have been successfully created/updated to the Cisco Catalyst Center + and their creation/updation has been verified.""".format(email_notification_name) + self.log(msg, "INFO") + else: + self.log("""Playbook's input does not match with Cisco Catalyst Center, indicating that Email Event Subscription Notification with + name '{0}' creation/updation task may not have executed successfully.""".format(email_notification_name), "INFO") + + if config.get('syslog_event_notification'): + syslog_notification_details = self.want.get('syslog_event_notification') + syslog_notification_name = syslog_notification_details.get('notification_name') + current_syslog_notifications = self.have.get("syslog_subscription_notifications") + is_syslog_notification_exist = False + + if current_syslog_notifications: + for notification in current_syslog_notifications: + if notification["name"] == syslog_notification_name: + is_syslog_notification_exist = True + break + + if is_syslog_notification_exist: + self.status = "success" + msg = """Requested Syslog Events Subscription Notification '{0}' have been successfully created/updated to the Cisco Catalyst Center + and their creation/updation has been verified.""".format(syslog_notification_name) + self.log(msg, "INFO") + else: + self.log("""Playbook's input does not match with Cisco Catalyst Center, indicating that Syslog Event Subscription Notification with + name '{0}' creation/updation task may not have executed successfully.""".format(syslog_notification_name), "INFO") + return self def verify_diff_deleted(self, config): @@ -2729,6 +5010,69 @@ def verify_diff_deleted(self, config): self.log("""Playbook's input does not match with Cisco Catalyst Center, indicating that ITSM Integration setting with name '{0}' deletion task may not have executed successfully.""".format(itsm_name), "INFO") + if config.get('webhook_event_notification'): + webhook_notification_details = self.want.get('webhook_event_notification') + web_notification_name = webhook_notification_details.get('notification_name') + current_webhook_notifications = self.have.get("webhook_subscription_notifications") + is_webhook_notification_deleted = True + + if current_webhook_notifications: + for notification in current_webhook_notifications: + if notification["name"] == web_notification_name: + is_webhook_notification_deleted = False + break + + if is_webhook_notification_deleted: + self.status = "success" + msg = """Requested Webhook Events Subscription Notification '{0}' have been successfully deleted from the Cisco Catalyst Center + and their deletion has been verified.""".format(web_notification_name) + self.log(msg, "INFO") + else: + self.log("""Playbook's input does not match with Cisco Catalyst Center, indicating that Webhook Events Subscription Notification + with name '{0}' deletion task may not have executed successfully.""".format(web_notification_name), "INFO") + + if config.get('email_event_notification'): + email_notification_details = self.want.get('email_event_notification') + email_notification_name = email_notification_details.get('notification_name') + current_email_notifications = self.have.get("email_subscription_notifications") + is_email_notification_deleted = True + + if current_email_notifications: + for notification in current_email_notifications: + if notification["name"] == email_notification_name: + is_email_notification_deleted = False + break + + if is_email_notification_deleted: + self.status = "success" + msg = """Requested Email Events Subscription Notification '{0}' have been successfully deleted from the Cisco Catalyst Center + and their deletion has been verified.""".format(email_notification_name) + self.log(msg, "INFO") + else: + self.log("""Playbook's input does not match with Cisco Catalyst Center, indicating that Email Events Subscription Notification + with name '{0}' deletion task may not have executed successfully.""".format(email_notification_name), "INFO") + + if config.get('syslog_event_notification'): + syslog_notification_details = self.want.get('syslog_event_notification') + syslog_notification_name = syslog_notification_details.get('notification_name') + current_syslog_notifications = self.have.get("syslog_subscription_notifications") + is_syslog_notification_deleted = True + + if current_syslog_notifications: + for notification in current_syslog_notifications: + if notification["name"] == syslog_notification_name: + is_syslog_notification_deleted = False + break + + if is_syslog_notification_deleted: + self.status = "success" + msg = """Requested Syslog Events Subscription Notification '{0}' have been successfully deleted from the Cisco Catalyst Center + and their deletion has been verified.""".format(syslog_notification_name) + self.log(msg, "INFO") + else: + self.log("""Playbook's input does not match with Cisco Catalyst Center, indicating that Syslog Events Subscription Notification + with name '{0}' deletion task may not have executed successfully.""".format(syslog_notification_name), "INFO") + return self From c515649c374ffa7b86262213854d8364c6a3ab79 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Sat, 1 Jun 2024 13:51:27 +0530 Subject: [PATCH 53/78] Removed the subscriber_name for the ISE server. Made required for minimal params for updation --- changelogs/changelog.yaml | 3 +- ...se_radius_integration_workflow_manager.yml | 1 - ...ise_radius_integration_workflow_manager.py | 313 ++++++++++-------- 3 files changed, 170 insertions(+), 147 deletions(-) diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index e033891697..6da0baa0e3 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -879,4 +879,5 @@ releases: - provision_workflow_manager - Updated changes related to handle errors. - site_workflow_manager - Updated changes in Site updation. - network_settings_workflow_manager - Added attributes 'ipv4_global_pool_name'. - - template_workflow_manager - Removed attributes 'create_time', 'failure_policy', 'last_update_time', 'latest_version_time', 'parent_template_id', 'project_id', 'validation_errors', 'rollback_template_params' and 'rollback_template_content'. \ No newline at end of file + - template_workflow_manager - Removed attributes 'create_time', 'failure_policy', 'last_update_time', 'latest_version_time', 'parent_template_id', 'project_id', 'validation_errors', 'rollback_template_params' and 'rollback_template_content'. + - ise_radius_integration_workflow_manager - Removed the attributes 'port' and 'subscriber_name'. \ No newline at end of file diff --git a/playbooks/ise_radius_integration_workflow_manager.yml b/playbooks/ise_radius_integration_workflow_manager.yml index 22a0e5ce28..fff66424d6 100644 --- a/playbooks/ise_radius_integration_workflow_manager.yml +++ b/playbooks/ise_radius_integration_workflow_manager.yml @@ -86,7 +86,6 @@ password: abcd fqdn: abc.cisco.com ip_address: 10.195.243.59 - subscriber_name: abcde description: CISCO ISE trusted_server: True diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index c1a537f792..66f1f0343d 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -175,11 +175,6 @@ - IP Address of the Cisco ISE Server. - Required for passing the cisco_ise_dtos. type: str - subscriber_name: - description: - - Subscriber name of the Cisco ISE server. - - Required for passing the cisco_ise_dtos. - type: str description: description: Description about the Cisco ISE server. type: str @@ -288,7 +283,6 @@ password: "12345" fqdn: abs.cisco.com ip_address: 10.0.0.2 - subscriber_name: px-1234 description: Cisco ISE trusted_server: True @@ -346,7 +340,6 @@ password: "12345" fqdn: abs.cisco.com ip_address: 10.0.0.2 - subscriber_name: px-1234 description: Cisco ISE - name: Delete an Authentication and Policy server. @@ -476,7 +469,6 @@ def validate_input(self): "password": {"type": 'string'}, "fqdn": {"type": 'string'}, "ip_address": {"type": 'string'}, - "subscriber_name": {"type": 'string'}, "description": {"type": 'string'}, "ssh_key": {"type": 'string'}, }, @@ -745,44 +737,51 @@ def get_want_authentication_policy_server(self, auth_policy_server): """ auth_server = {} + auth_server_exists = self.have.get("authenticationPolicyServer").get("exists") + auth_server_details = self.have.get("authenticationPolicyServer").get("details") trusted_server = False - server_type = auth_policy_server.get("server_type") - if server_type not in ["ISE", "AAA", None]: - self.msg = "server_type should either be ISE or AAA but not {0}.".format(server_type) - self.status = "failed" - return self + if not auth_server_exists: + server_type = auth_policy_server.get("server_type") + if server_type not in ["ISE", "AAA", None]: + self.msg = "server_type should either be ISE or AAA but not {0}.".format(server_type) + self.status = "failed" + return self - if server_type == "ISE": - auth_server.update({"isIseEnabled": True}) + if server_type == "ISE": + auth_server.update({"isIseEnabled": True}) + else: + auth_server.update({"isIseEnabled": False}) else: - auth_server.update({"isIseEnabled": False}) + auth_server.update({"isIseEnabled": auth_server_details.get("isIseEnabled")}) auth_server.update({"ipAddress": auth_policy_server.get("server_ip_address")}) auth_server_exists = self.have.get("authenticationPolicyServer").get("exists") - shared_secret = auth_policy_server.get("shared_secret") - if not (shared_secret or auth_server_exists): - self.msg = "Missing parameter 'shared_secret' is required." - self.status = "failed" - return self - shared_secret = str(shared_secret) - if not (4 <= len(shared_secret) <= 100): - self.msg = "The 'shared_secret' should contain between 4 and 100 characters." - self.status = "failed" - return self + if not auth_server_exists: + shared_secret = auth_policy_server.get("shared_secret") + if not (shared_secret or auth_server_exists): + self.msg = "Missing parameter 'shared_secret' is required." + self.status = "failed" + return self - if " " in shared_secret: - self.msg = "The 'shared_secret' should not contain any spaces." - self.status = "failed" - return self + shared_secret = str(shared_secret) + if not (4 <= len(shared_secret) <= 100): + self.msg = "The 'shared_secret' should contain between 4 and 100 characters." + self.status = "failed" + return self - if "?" in shared_secret or "<" in shared_secret: - self.msg = "The 'shared_secret' should not contain '?' or '<' characters." - self.status = "failed" - return self + if " " in shared_secret: + self.msg = "The 'shared_secret' should not contain any spaces." + self.status = "failed" + return self + + if "?" in shared_secret or "<" in shared_secret: + self.msg = "The 'shared_secret' should not contain '?' or '<' characters." + self.status = "failed" + return self - auth_server.update({"sharedSecret": shared_secret}) + auth_server.update({"sharedSecret": shared_secret}) protocol = auth_policy_server.get("protocol") if protocol not in ["RADIUS", "TACACS", "RADIUS_TACACS", None]: @@ -794,122 +793,141 @@ def get_want_authentication_policy_server(self, auth_policy_server): if protocol is not None: auth_server.update({"protocol": protocol}) else: - auth_server.update({"protocol": "RADIUS"}) + if not auth_server_exists: + auth_server.update({"protocol": "RADIUS"}) + else: + auth_server.update({"protocol": auth_server_details.get("protocol")}) auth_server.update({"port": 49}) - encryption_scheme = auth_policy_server.get("encryption_scheme") - if encryption_scheme not in ["KEYWRAP", "RADSEC", None]: - self.msg = "encryption_scheme should be in ['KEYWRAP', 'RADSEC']. " + \ - "It should not be {0}.".format(encryption_scheme) - self.status = "failed" - return self - - if encryption_scheme: - auth_server.update({"encryptionScheme": encryption_scheme}) - - if encryption_scheme == "KEYWRAP": - message_key = auth_policy_server.get("message_authenticator_code_key") - if not message_key: - self.msg = "The 'message_authenticator_code_key' should not be empty if the encryption_scheme is 'KEYWRAP'." + if not auth_server_exists: + encryption_scheme = auth_policy_server.get("encryption_scheme") + if encryption_scheme not in ["KEYWRAP", "RADSEC", None]: + self.msg = "encryption_scheme should be in ['KEYWRAP', 'RADSEC']. " + \ + "It should not be {0}.".format(encryption_scheme) self.status = "failed" return self - message_key = str(message_key) - message_key_length = len(message_key) - if message_key_length != 20: - self.msg = "The 'message_authenticator_code_key' should be exactly 20 characters." - self.status = "failed" - return self + if encryption_scheme: + auth_server.update({"encryptionScheme": encryption_scheme}) - auth_server.update({"messageKey": message_key}) + if encryption_scheme == "KEYWRAP": + message_key = auth_policy_server.get("message_authenticator_code_key") + if not message_key: + self.msg = "The 'message_authenticator_code_key' should not be empty if the encryption_scheme is 'KEYWRAP'." + self.status = "failed" + return self - encryption_key = auth_policy_server.get("encryption_key") - if not encryption_key: - self.msg = "encryption_key should not be empty if encryption_scheme is 'KEYWRAP'." - self.status = "failed" - return self + message_key = str(message_key) + message_key_length = len(message_key) + if message_key_length != 20: + self.msg = "The 'message_authenticator_code_key' should be exactly 20 characters." + self.status = "failed" + return self - encryption_key = str(encryption_key) - encryption_key_length = len(encryption_key) - if encryption_key_length != 16: - self.msg = "The 'encryption_key' must be 16 characters long. It may contain alphanumeric and special characters." - self.status = "failed" - return self + auth_server.update({"messageKey": message_key}) - auth_server.update({"encryptionKey": encryption_key}) + encryption_key = auth_policy_server.get("encryption_key") + if not encryption_key: + self.msg = "encryption_key should not be empty if encryption_scheme is 'KEYWRAP'." + self.status = "failed" + return self - authentication_port = auth_policy_server.get("authentication_port") - if not authentication_port: - authentication_port = 1812 + encryption_key = str(encryption_key) + encryption_key_length = len(encryption_key) + if encryption_key_length != 16: + self.msg = "The 'encryption_key' must be 16 characters long. It may contain alphanumeric and special characters." + self.status = "failed" + return self - if not str(authentication_port).isdigit(): - self.msg = "The 'authentication_port' should contain only digits." - self.status = "failed" - return self + auth_server.update({"encryptionKey": encryption_key}) - if not 1 <= authentication_port <= 65535: - self.msg = "The 'authentication_port' should be from 1 to 65535." - self.status = "failed" - return self + if not auth_server_exists: + authentication_port = auth_policy_server.get("authentication_port") + if not authentication_port: + authentication_port = 1812 - auth_server.update({"authenticationPort": authentication_port}) + if not str(authentication_port).isdigit(): + self.msg = "The 'authentication_port' should contain only digits." + self.status = "failed" + return self - accounting_port = auth_policy_server.get("accounting_port") - if not accounting_port: - accounting_port = 1813 + if not 1 <= authentication_port <= 65535: + self.msg = "The 'authentication_port' should be from 1 to 65535." + self.status = "failed" + return self - if not str(accounting_port).isdigit(): - self.msg = "The 'accounting_port' should contain only digits." - self.status = "failed" - return self + auth_server.update({"authenticationPort": authentication_port}) + else: + auth_server.update({"authenticationPort": auth_server_details.get("authenticationPort")}) - if not 1 <= accounting_port <= 65535: - self.msg = "The 'accounting_port' should be from 1 to 65535." - self.status = "failed" - return self + if not auth_server_exists: + accounting_port = auth_policy_server.get("accounting_port") + if not accounting_port: + accounting_port = 1813 - auth_server.update({"accountingPort": accounting_port}) + if not str(accounting_port).isdigit(): + self.msg = "The 'accounting_port' should contain only digits." + self.status = "failed" + return self + + if not 1 <= accounting_port <= 65535: + self.msg = "The 'accounting_port' should be from 1 to 65535." + self.status = "failed" + return self + + auth_server.update({"accountingPort": accounting_port}) + else: + auth_server.update({"accountingPort": auth_server_details.get("accountingPort")}) retries = auth_policy_server.get("retries") if not retries: - retries = "3" - - retries = str(retries) - if not retries.isdigit(): - self.msg = "The 'retries' should contain only from 0-9." - self.status = "failed" - return self + if not auth_server_exists: + auth_server.update({"retries": "3"}) + else: + auth_server.update({"retries": auth_server_details.get("retries")}) + else: + retries = str(retries) + if not retries.isdigit(): + self.msg = "The 'retries' should contain only from 0-9." + self.status = "failed" + return self - if not 1 <= int(retries) <= 3: - self.msg = "The 'retries' should be from 1 to 3." - self.status = "failed" - return self + if not 1 <= int(retries) <= 3: + self.msg = "The 'retries' should be from 1 to 3." + self.status = "failed" + return self - auth_server.update({"retries": retries}) + auth_server.update({"retries": retries}) timeout = auth_policy_server.get("timeout") if not timeout: - timeout = "4" - - timeout = str(timeout) - if not timeout.isdigit(): - self.msg = "The 'timeout' should contain only from 0-9." - self.status = "failed" - return self + if not auth_server_exists: + auth_server.update({"timeoutSeconds": "4"}) + else: + auth_server.update({"timeoutSeconds": auth_server_details.get("timeoutSeconds")}) + else: + timeout = str(timeout) + if not timeout.isdigit(): + self.msg = "The 'timeout' should contain only from 0-9." + self.status = "failed" + return self - if not 2 <= int(timeout) <= 20: - self.msg = "The 'timeout' should be from 2 to 20." - self.status = "failed" - return self + if not 2 <= int(timeout) <= 20: + self.msg = "The 'timeout' should be from 2 to 20." + self.status = "failed" + return self - auth_server.update({"timeoutSeconds": timeout}) + auth_server.update({"timeoutSeconds": timeout}) - role = auth_policy_server.get("role") - if role: - auth_server.update({"role": role}) + if not auth_server_exists: + role = auth_policy_server.get("role") + if role: + auth_server.update({"role": role}) + else: + auth_server.update({"role": "secondary"}) else: - auth_server.update({"role": "secondary"}) + auth_server.update({"role": auth_server_details.get("role")}) if auth_server.get("isIseEnabled"): cisco_ise_dtos = auth_policy_server.get("cisco_ise_dtos") @@ -966,15 +984,14 @@ def get_want_authentication_policy_server(self, auth_policy_server): "ipAddress": ip_address }) - subscriber_name = ise_credential.get("subscriber_name") - if not subscriber_name: - self.msg = "Missing parameter 'subscriber_name' is required when server_type is ISE." - self.status = "failed" - return self - - auth_server.get("ciscoIseDtos")[position_ise_creds].update({ - "subscriberName": subscriber_name - }) + if not auth_server_exists: + auth_server.get("ciscoIseDtos")[position_ise_creds].update({ + "subscriberName": "ersadmin" + }) + else: + auth_server.get("ciscoIseDtos")[position_ise_creds].update({ + "subscriberName": auth_server_details.get("ciscoIseDtos")[0].get("subscriberName") + }) description = ise_credential.get("description") if description: @@ -991,16 +1008,22 @@ def get_want_authentication_policy_server(self, auth_policy_server): position_ise_creds += 1 pxgrid_enabled = auth_policy_server.get("pxgrid_enabled") - if pxgrid_enabled is not None: - auth_server.update({"pxgridEnabled": pxgrid_enabled}) + if not pxgrid_enabled: + if not auth_server_exists: + auth_server.update({"pxgridEnabled": True}) + else: + auth_server.update({"pxgridEnabled": auth_server_details.get("pxgridEnabled")}) else: - auth_server.update({"pxgridEnabled": True}) + auth_server.update({"pxgridEnabled": pxgrid_enabled}) use_dnac_cert_for_pxgrid = auth_policy_server.get("use_dnac_cert_for_pxgrid") - if use_dnac_cert_for_pxgrid: - auth_server.update({"useDnacCertForPxgrid": use_dnac_cert_for_pxgrid}) + if not use_dnac_cert_for_pxgrid: + if not auth_server_exists: + auth_server.update({"useDnacCertForPxgrid": False}) + else: + auth_server.update({"useDnacCertForPxgrid": auth_server_details.get("useDnacCertForPxgrid")}) else: - auth_server.update({"useDnacCertForPxgrid": False}) + auth_server.update({"useDnacCertForPxgrid": use_dnac_cert_for_pxgrid}) external_cisco_ise_ip_addr_dtos = auth_policy_server \ .get("external_cisco_ise_ip_addr_dtos") @@ -1119,14 +1142,14 @@ def format_payload_for_update(self, have_auth_server, want_auth_server): self - The current object with updated desired Authentication Policy Server information. """ - if want_auth_server.get("sharedSecret") is not None: - del want_auth_server["sharedSecret"] - if want_auth_server.get("encryptionScheme") is not None: - del want_auth_server["encryptionScheme"] - if want_auth_server.get("messageKey") is not None: - del want_auth_server["messageKey"] - if want_auth_server.get("encryptionKey") is not None: - del want_auth_server["encryptionKey"] + # if want_auth_server.get("sharedSecret") is not None: + # del want_auth_server["sharedSecret"] + # if want_auth_server.get("encryptionScheme") is not None: + # del want_auth_server["encryptionScheme"] + # if want_auth_server.get("messageKey") is not None: + # del want_auth_server["messageKey"] + # if want_auth_server.get("encryptionKey") is not None: + # del want_auth_server["encryptionKey"] update_params = ["authenticationPort", "accountingPort", "role"] for item in update_params: @@ -1190,7 +1213,7 @@ def update_auth_policy_server(self, ipAddress): if is_ise_server: trusted_server = self.want.get("trusted_server") self.accept_cisco_ise_server_certificate(ipAddress, trusted_server) - time.sleep(15) + time.sleep(20) response = self.dnac._exec( family="system_settings", function='get_authentication_and_policy_servers', From 6d1236f2f8b3e1dbf68ad18ebc01c699dfbb1b1f Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Sat, 1 Jun 2024 16:40:47 -0700 Subject: [PATCH 54/78] Addressed review comments --- .../network_compliance_workflow_manager.py | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/plugins/modules/network_compliance_workflow_manager.py b/plugins/modules/network_compliance_workflow_manager.py index 1180c42cdb..fe523a6821 100644 --- a/plugins/modules/network_compliance_workflow_manager.py +++ b/plugins/modules/network_compliance_workflow_manager.py @@ -479,14 +479,17 @@ def validate_run_compliance_paramters(self, mgmt_ip_instance_id_map, run_complia # compliance_detail_params compliance_detail_params["deviceUuids"] = ",".join(list(mgmt_ip_instance_id_map.values())) - # Verify if there are any devices with Compliance Status of "IN_PROGRESS" + # Check for devices with Compliance Status of "IN_PROGRESS" and update parameters accordingly if run_compliance_params: device_in_progress = [] response = self.get_compliance_detail(compliance_detail_params) if not response: - msg = "Error occurred when retrieving Compliance Report to identify if there are devices with 'IN_PROGRESS' status" - msg += "is required on device(s): {0}".format(list(mgmt_ip_instance_id_map.keys())) + msg = ( + "Error occurred when retrieving Compliance Report to identify if there are " + "devices with 'IN_PROGRESS' status. This is required on device(s): {0}" + .format(list(mgmt_ip_instance_id_map.keys())) + ) self.log(msg) self.module.fail_json(msg) @@ -499,11 +502,8 @@ def validate_run_compliance_paramters(self, mgmt_ip_instance_id_map, run_complia if device_in_progress: # Update run_compliance_params to exclude devices with 'IN_PROGRESS' status run_compliance_params["deviceUuids"] = [device_id for device_id in mgmt_ip_instance_id_map.values() if device_id not in device_in_progress] - self.log( - "Updated the run_compliance_params to exclude devices with a compliance status of 'IN_PROGRESS'." - "run_compliance_params: {0}".format(run_compliance_params), - "DEBUG" - ) + msg = "Excluding 'IN_PROGRESS' devices from compliance check. Updated run_compliance_params: {0}".format(run_compliance_params) + self.log(message, "DEBUG") return run_compliance_params, compliance_detail_params @@ -845,12 +845,8 @@ def get_want(self, config): if excluded_device_ids: # Exclude devices in the "OTHER" category from sync_device_config_params sync_device_config_params["deviceId"] = [device_id for device_id in mgmt_ip_instance_id_map.values() if device_id not in excluded_device_ids] - self.log( - "Skipping these devices because their compliance status is not 'NON_COMPLIANT': {0}".format( - categorized_devices.get("OTHER") - ), - "WARNING" - ) + message = "Skipping these devices because their compliance status is not 'NON_COMPLIANT': {0}".format(', '.join(excluded_device_ids)) + self.log(message, "WARNING") # Construct the "want" dictionary containing the desired state parameters want = {} From 8302b3e6ed7de6eff3456f04d01660c9cb8263dd Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Sat, 1 Jun 2024 16:47:47 -0700 Subject: [PATCH 55/78] Addressed review comments --- plugins/modules/network_compliance_workflow_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/network_compliance_workflow_manager.py b/plugins/modules/network_compliance_workflow_manager.py index fe523a6821..460cac5f87 100644 --- a/plugins/modules/network_compliance_workflow_manager.py +++ b/plugins/modules/network_compliance_workflow_manager.py @@ -503,7 +503,7 @@ def validate_run_compliance_paramters(self, mgmt_ip_instance_id_map, run_complia # Update run_compliance_params to exclude devices with 'IN_PROGRESS' status run_compliance_params["deviceUuids"] = [device_id for device_id in mgmt_ip_instance_id_map.values() if device_id not in device_in_progress] msg = "Excluding 'IN_PROGRESS' devices from compliance check. Updated run_compliance_params: {0}".format(run_compliance_params) - self.log(message, "DEBUG") + self.log(msg, "DEBUG") return run_compliance_params, compliance_detail_params @@ -845,8 +845,8 @@ def get_want(self, config): if excluded_device_ids: # Exclude devices in the "OTHER" category from sync_device_config_params sync_device_config_params["deviceId"] = [device_id for device_id in mgmt_ip_instance_id_map.values() if device_id not in excluded_device_ids] - message = "Skipping these devices because their compliance status is not 'NON_COMPLIANT': {0}".format(', '.join(excluded_device_ids)) - self.log(message, "WARNING") + msg = "Skipping these devices because their compliance status is not 'NON_COMPLIANT': {0}".format(excluded_device_ids) + self.log(msg, "WARNING") # Construct the "want" dictionary containing the desired state parameters want = {} From 69467dab21cad1ec32da18c7a5d3c6798bcffbc5 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Mon, 3 Jun 2024 11:49:50 +0530 Subject: [PATCH 56/78] Addressed the review comments --- changelogs/changelog.yaml | 2 +- ...se_radius_integration_workflow_manager.yml | 1 + ...ise_radius_integration_workflow_manager.py | 155 ++++++++++-------- 3 files changed, 92 insertions(+), 66 deletions(-) diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 6da0baa0e3..7d4450dfcc 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -880,4 +880,4 @@ releases: - site_workflow_manager - Updated changes in Site updation. - network_settings_workflow_manager - Added attributes 'ipv4_global_pool_name'. - template_workflow_manager - Removed attributes 'create_time', 'failure_policy', 'last_update_time', 'latest_version_time', 'parent_template_id', 'project_id', 'validation_errors', 'rollback_template_params' and 'rollback_template_content'. - - ise_radius_integration_workflow_manager - Removed the attributes 'port' and 'subscriber_name'. \ No newline at end of file + - ise_radius_integration_workflow_manager - Removed the attributes 'port' and 'subscriber_name'. Added the attribute 'ise_integration_wait_time'. \ No newline at end of file diff --git a/playbooks/ise_radius_integration_workflow_manager.yml b/playbooks/ise_radius_integration_workflow_manager.yml index fff66424d6..9ca9a9181a 100644 --- a/playbooks/ise_radius_integration_workflow_manager.yml +++ b/playbooks/ise_radius_integration_workflow_manager.yml @@ -88,6 +88,7 @@ ip_address: 10.195.243.59 description: CISCO ISE trusted_server: True + ise_integration_wait_time: 20 - name: Delete an ISE Server. cisco.dnac.ise_radius_integration_workflow_manager: diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index 66f1f0343d..6168f1f91c 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -201,7 +201,14 @@ description: - Indicates whether the certificate is trustworthy for the server. - Serves as a validation of its authenticity and reliability in secure connections. + default: True type: bool + ise_integration_wait_time: + description: + - Indicates the sleep time after initiating the Cisco ISE integration process. + - Maximum sleep time should be less or equal to 60 seconds. + default: 20 + type: int requirements: - dnacentersdk >= 2.7.0 - python >= 3.9 @@ -285,6 +292,7 @@ ip_address: 10.0.0.2 description: Cisco ISE trusted_server: True + ise_integration_wait_time: 20 - name: Update an AAA server. cisco.dnac.ise_radius_integration_workflow_manager: @@ -479,7 +487,9 @@ def validate_input(self): "external_ip_address": {"type": 'string'}, }, "ise_type": {"type": 'string'}, - } + }, + "trusted_server": {"type": 'bool'}, + "ise_integration_wait_time": {"type": 'integer'} } } @@ -743,7 +753,7 @@ def get_want_authentication_policy_server(self, auth_policy_server): if not auth_server_exists: server_type = auth_policy_server.get("server_type") if server_type not in ["ISE", "AAA", None]: - self.msg = "server_type should either be ISE or AAA but not {0}.".format(server_type) + self.msg = "The server_type should either be ISE or AAA but not {0}.".format(server_type) self.status = "failed" return self @@ -760,7 +770,7 @@ def get_want_authentication_policy_server(self, auth_policy_server): if not auth_server_exists: shared_secret = auth_policy_server.get("shared_secret") - if not (shared_secret or auth_server_exists): + if not shared_secret: self.msg = "Missing parameter 'shared_secret' is required." self.status = "failed" return self @@ -771,15 +781,12 @@ def get_want_authentication_policy_server(self, auth_policy_server): self.status = "failed" return self - if " " in shared_secret: - self.msg = "The 'shared_secret' should not contain any spaces." - self.status = "failed" - return self - - if "?" in shared_secret or "<" in shared_secret: - self.msg = "The 'shared_secret' should not contain '?' or '<' characters." - self.status = "failed" - return self + invalid_chars = " ?<" + for char in invalid_chars: + if char in shared_secret: + self.msg = "The 'shared_secret' should not contain spaces or the characters '?', '<'." + self.status = "failed" + return self auth_server.update({"sharedSecret": shared_secret}) @@ -803,7 +810,7 @@ def get_want_authentication_policy_server(self, auth_policy_server): if not auth_server_exists: encryption_scheme = auth_policy_server.get("encryption_scheme") if encryption_scheme not in ["KEYWRAP", "RADSEC", None]: - self.msg = "encryption_scheme should be in ['KEYWRAP', 'RADSEC']. " + \ + self.msg = "The encryption_scheme should be in ['KEYWRAP', 'RADSEC']. " + \ "It should not be {0}.".format(encryption_scheme) self.status = "failed" return self @@ -829,7 +836,7 @@ def get_want_authentication_policy_server(self, auth_policy_server): encryption_key = auth_policy_server.get("encryption_key") if not encryption_key: - self.msg = "encryption_key should not be empty if encryption_scheme is 'KEYWRAP'." + self.msg = "The encryption_key should not be empty if encryption_scheme is 'KEYWRAP'." self.status = "failed" return self @@ -887,47 +894,51 @@ def get_want_authentication_policy_server(self, auth_policy_server): else: auth_server.update({"retries": auth_server_details.get("retries")}) else: - retries = str(retries) - if not retries.isdigit(): + try: + retries = str(retries) + if not 1 <= int(retries) <= 3: + self.msg = "The 'retries' should be from 1 to 3." + self.status = "failed" + return self + except ValueError: self.msg = "The 'retries' should contain only from 0-9." self.status = "failed" return self - if not 1 <= int(retries) <= 3: - self.msg = "The 'retries' should be from 1 to 3." - self.status = "failed" - return self - auth_server.update({"retries": retries}) timeout = auth_policy_server.get("timeout") - if not timeout: - if not auth_server_exists: - auth_server.update({"timeoutSeconds": "4"}) - else: - auth_server.update({"timeoutSeconds": auth_server_details.get("timeoutSeconds")}) + if not auth_server_exists: + default_timeout = "4" else: - timeout = str(timeout) - if not timeout.isdigit(): - self.msg = "The 'timeout' should contain only from 0-9." - self.status = "failed" - return self + default_timeout = str(auth_server_details.get("timeoutSeconds")) - if not 2 <= int(timeout) <= 20: - self.msg = "The 'timeout' should be from 2 to 20." + # If 'timeout' is not provided, use 'default_timeout' + if timeout is None: + auth_server.update({"timeoutSeconds": default_timeout}) + else: + try: + timeout_int = int(timeout) + if timeout_int < 2 or timeout_int > 20: + self.msg = "The 'timeout' should be from 2 to 20." + self.status = "failed" + return self + + auth_server.update({"timeoutSeconds": str(timeout)}) + except ValueError: + self.msg = "The 'time_out' must contain only digits." self.status = "failed" return self - auth_server.update({"timeoutSeconds": timeout}) - + # Determine the role based on whether the auth server exists and if the role is specified if not auth_server_exists: - role = auth_policy_server.get("role") - if role: - auth_server.update({"role": role}) - else: - auth_server.update({"role": "secondary"}) + # Use the role from 'auth_policy_server' if available, otherwise default to "secondary" + role = auth_policy_server.get("role", "secondary") else: - auth_server.update({"role": auth_server_details.get("role")}) + # Use the role from 'auth_server_details' + role = auth_server_details.get("role") + + auth_server.update({"role": role}) if auth_server.get("isIseEnabled"): cisco_ise_dtos = auth_policy_server.get("cisco_ise_dtos") @@ -1008,22 +1019,22 @@ def get_want_authentication_policy_server(self, auth_policy_server): position_ise_creds += 1 pxgrid_enabled = auth_policy_server.get("pxgrid_enabled") - if not pxgrid_enabled: - if not auth_server_exists: - auth_server.update({"pxgridEnabled": True}) + if pxgrid_enabled is None: + if auth_server_exists: + pxgrid_enabled = auth_server_details.get("pxgridEnabled") else: - auth_server.update({"pxgridEnabled": auth_server_details.get("pxgridEnabled")}) - else: - auth_server.update({"pxgridEnabled": pxgrid_enabled}) + pxgrid_enabled = True + + auth_server.update({"pxgridEnabled": pxgrid_enabled}) use_dnac_cert_for_pxgrid = auth_policy_server.get("use_dnac_cert_for_pxgrid") - if not use_dnac_cert_for_pxgrid: - if not auth_server_exists: - auth_server.update({"useDnacCertForPxgrid": False}) + if use_dnac_cert_for_pxgrid is None: + if auth_server_exists: + use_dnac_cert_for_pxgrid = auth_server_details.get("useDnacCertForPxgrid") else: - auth_server.update({"useDnacCertForPxgrid": auth_server_details.get("useDnacCertForPxgrid")}) - else: - auth_server.update({"useDnacCertForPxgrid": use_dnac_cert_for_pxgrid}) + use_dnac_cert_for_pxgrid = False + + auth_server.update({"useDnacCertForPxgrid": use_dnac_cert_for_pxgrid}) external_cisco_ise_ip_addr_dtos = auth_policy_server \ .get("external_cisco_ise_ip_addr_dtos") @@ -1052,13 +1063,35 @@ def get_want_authentication_policy_server(self, auth_policy_server): .update({"type": ise_type}) position_ise_addresses += 1 - if auth_policy_server.get("trusted_server"): + trusted_server = auth_policy_server.get("trusted_server") + if auth_policy_server.get("trusted_server") is None: trusted_server = True + else: + trusted_server = auth_policy_server.get("trusted_server") + + self.want.update({"trusted_server": trusted_server}) + + ise_integration_wait_time = auth_policy_server.get("ise_integration_wait_time") + if ise_integration_wait_time is None: + ise_integration_wait_time = 20 + else: + try: + ise_integration_wait_time_int = int(ise_integration_wait_time) + if ise_integration_wait_time_int < 1 or ise_integration_wait_time_int > 60: + self.msg = "The ise_integration_wait_time should be from 1 to 60 seconds." + self.status = "failed" + return self + + except ValueError: + self.msg = "The 'ise_integration_wait_time' should contain only digits." + self.status = "failed" + return self + + self.want.update({"ise_integration_wait_time": ise_integration_wait_time}) self.log("Authentication and Policy Server playbook details: {0}" .format(auth_server), "DEBUG") self.want.update({"authenticationPolicyServer": auth_server}) - self.want.update({"trusted_server": trusted_server}) self.msg = "Collecting the Authentication and Policy Server details from the playbook" self.status = "success" return self @@ -1142,15 +1175,6 @@ def format_payload_for_update(self, have_auth_server, want_auth_server): self - The current object with updated desired Authentication Policy Server information. """ - # if want_auth_server.get("sharedSecret") is not None: - # del want_auth_server["sharedSecret"] - # if want_auth_server.get("encryptionScheme") is not None: - # del want_auth_server["encryptionScheme"] - # if want_auth_server.get("messageKey") is not None: - # del want_auth_server["messageKey"] - # if want_auth_server.get("encryptionKey") is not None: - # del want_auth_server["encryptionKey"] - update_params = ["authenticationPort", "accountingPort", "role"] for item in update_params: have_auth_server_item = have_auth_server.get(item) @@ -1213,7 +1237,8 @@ def update_auth_policy_server(self, ipAddress): if is_ise_server: trusted_server = self.want.get("trusted_server") self.accept_cisco_ise_server_certificate(ipAddress, trusted_server) - time.sleep(20) + ise_integration_wait_time = self.want.get("ise_integration_wait_time") + time.sleep(ise_integration_wait_time) response = self.dnac._exec( family="system_settings", function='get_authentication_and_policy_servers', From 1bc101aa5b7f037e0043cfb5ea6b89848afb90b9 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Mon, 3 Jun 2024 13:06:28 +0530 Subject: [PATCH 57/78] Addressed the review comments --- plugins/modules/ise_radius_integration_workflow_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index 6168f1f91c..aad8187bb3 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -895,8 +895,8 @@ def get_want_authentication_policy_server(self, auth_policy_server): auth_server.update({"retries": auth_server_details.get("retries")}) else: try: - retries = str(retries) - if not 1 <= int(retries) <= 3: + retries_int = int(retries) + if not 1 <= retries_int <= 3: self.msg = "The 'retries' should be from 1 to 3." self.status = "failed" return self @@ -905,7 +905,7 @@ def get_want_authentication_policy_server(self, auth_policy_server): self.status = "failed" return self - auth_server.update({"retries": retries}) + auth_server.update({"retries": str(retries)}) timeout = auth_policy_server.get("timeout") if not auth_server_exists: From 591d88881eaa246e78df0654736be237e6dbda98 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Mon, 3 Jun 2024 13:25:12 +0530 Subject: [PATCH 58/78] Addressed the review comments --- .../modules/ise_radius_integration_workflow_manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index aad8187bb3..acf475a0ff 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -776,7 +776,7 @@ def get_want_authentication_policy_server(self, auth_policy_server): return self shared_secret = str(shared_secret) - if not (4 <= len(shared_secret) <= 100): + if len(shared_secret) < 4 or len(shared_secret) > 100: self.msg = "The 'shared_secret' should contain between 4 and 100 characters." self.status = "failed" return self @@ -859,7 +859,7 @@ def get_want_authentication_policy_server(self, auth_policy_server): self.status = "failed" return self - if not 1 <= authentication_port <= 65535: + if authentication_port < 1 or authentication_port > 65535: self.msg = "The 'authentication_port' should be from 1 to 65535." self.status = "failed" return self @@ -878,7 +878,7 @@ def get_want_authentication_policy_server(self, auth_policy_server): self.status = "failed" return self - if not 1 <= accounting_port <= 65535: + if accounting_port < 1 or accounting_port > 65535: self.msg = "The 'accounting_port' should be from 1 to 65535." self.status = "failed" return self @@ -896,7 +896,7 @@ def get_want_authentication_policy_server(self, auth_policy_server): else: try: retries_int = int(retries) - if not 1 <= retries_int <= 3: + if retries_int < 1 or retries_int > 3: self.msg = "The 'retries' should be from 1 to 3." self.status = "failed" return self From 0482b17ad7ed278ac556b176c10f0307ab7ee40d Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Mon, 3 Jun 2024 17:08:40 +0530 Subject: [PATCH 59/78] remove is_valid_ipv4 function as we are validating server address with is_valid_server_address API --- .../events_and_notifications_workflow_manager.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/plugins/modules/events_and_notifications_workflow_manager.py b/plugins/modules/events_and_notifications_workflow_manager.py index 065d39fd01..23b9ce3ed2 100644 --- a/plugins/modules/events_and_notifications_workflow_manager.py +++ b/plugins/modules/events_and_notifications_workflow_manager.py @@ -1294,12 +1294,6 @@ def add_syslog_destination(self, syslog_details): server_address = syslog_details.get('server_address') protocol = syslog_details.get('protocol') - if not self.is_valid_ipv4(server_address): - self.status = "failed" - self.msg = "Invalid server adderess '{0}' given in the playbook for configuring syslog destination".format(server_address) - self.log(self.msg, "ERROR") - return self - if not protocol: self.status = "failed" self.msg = "Protocol is needed while configuring the syslog destionation with name '{0}' in Cisco Catalyst Center".format(name) @@ -1392,13 +1386,6 @@ def update_syslog_destination(self, syslog_details, syslog_details_in_ccc): self.log(self.msg, "ERROR") return self - server_address = update_syslog_params.get('host') - if not self.is_valid_ipv4(server_address): - self.status = "failed" - self.msg = "Invalid server adderess '{0}' given in the playbook for updating syslog destination".format(server_address) - self.log(self.msg, "ERROR") - return self - response = self.dnac._exec( family="event_management", function='update_syslog_destination', From e62d8eca6a003493d8a7e3e4e157dc85d92239f1 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Mon, 3 Jun 2024 22:17:54 +0530 Subject: [PATCH 60/78] Changed notification_name to name while creating/updating/deleting the notification and make same chnages in playbook as well. --- ...nts_and_notifications_workflow_manager.yml | 10 +- ...ents_and_notifications_workflow_manager.py | 96 +++++++++---------- 2 files changed, 52 insertions(+), 54 deletions(-) diff --git a/playbooks/events_and_notifications_workflow_manager.yml b/playbooks/events_and_notifications_workflow_manager.yml index 9284912efe..8d38c0fd5a 100644 --- a/playbooks/events_and_notifications_workflow_manager.yml +++ b/playbooks/events_and_notifications_workflow_manager.yml @@ -63,14 +63,14 @@ webhook_event_notification: notification_name: "{{item.webhook_event_notification.notification_name}}" description: "{{item.webhook_event_notification.description}}" - webhook_dest_name: "{{item.webhook_event_notification.webhook_dest_name}}" - events_name_list: "{{item.webhook_event_notification.events_name_list}}" site_name_list: "{{item.webhook_event_notification.site_name_list}}" + events_name_list: "{{item.webhook_event_notification.events_name_list}}" + webhook_dest_name: "{{item.webhook_event_notification.webhook_dest_name}}" email_event_notification: notification_name: "{{item.email_event_notification.notification_name}}" description: "{{item.email_event_notification.description}}" - events_name_list: "{{item.email_event_notification.events_name_list}}" site_name_list: "{{item.email_event_notification.site_name_list}}" + events_name_list: "{{item.email_event_notification.events_name_list}}" from_email_address: "{{item.email_event_notification.from_email_address}}" to_email_addresses: "{{item.email_event_notification.to_email_addresses}}" subject: "{{item.email_event_notification.subject}}" @@ -79,9 +79,9 @@ syslog_event_notification: notification_name: "{{item.syslog_event_notification.notification_name}}" description: "{{item.syslog_event_notification.description}}" - syslog_dest_name: "{{item.syslog_event_notification.syslog_dest_name}}" - events_name_list: "{{item.syslog_event_notification.events_name_list}}" site_name_list: "{{item.syslog_event_notification.site_name_list}}" + events_name_list: "{{item.syslog_event_notification.events_name_list}}" + syslog_dest_name: "{{item.syslog_event_notification.syslog_dest_name}}" with_items: "{{ events_notification }}" tags: diff --git a/plugins/modules/events_and_notifications_workflow_manager.py b/plugins/modules/events_and_notifications_workflow_manager.py index 23b9ce3ed2..f973654066 100644 --- a/plugins/modules/events_and_notifications_workflow_manager.py +++ b/plugins/modules/events_and_notifications_workflow_manager.py @@ -300,7 +300,7 @@ Center. type: dict suboptions: - notification_name: + name: description: Name of the Webhook event subscription notification . type: str required: True @@ -355,7 +355,7 @@ Center. Here you can create the email subscription notification as well as the create/update the email instance as well. type: dict suboptions: - notification_name: + name: description: Name of the Webhook event subscription notification . type: str required: True @@ -426,7 +426,7 @@ Center. type: dict suboptions: - notification_name: + name: description: Name of the Syslog event subscription notification . type: str required: True @@ -724,11 +724,11 @@ state: merged config: - webhook_event_notification: - notification_name: "Webhook Notification." + name: "Webhook Notification." description: "Notification for webhook events subscription" - webhook_dest_name: "Webhook Demo" - events_name_list: ["AP Flap", "AP Reboot Crash"] site_name_list: ["Global/India", "Global/USA"] + events_name_list: ["AP Flap", "AP Reboot Crash"] + webhook_dest_name: "Webhook Demo" - name: Updating Webhook Notification with the list of names of subscribed events in the system. cisco.dnac.events_and_notifications_workflow_manager: @@ -744,10 +744,10 @@ state: merged config: - webhook_event_notification: - notification_name: "Webhook Notification." + name: "Webhook Notification." description: "Updated notification for webhook events subscription" - events_name_list: ["AP Flap", "AP Reboot Crash"] site_name_list: ["Global/India", "Global/USA", "Global/China"] + webhook_dest_name: "Webhook Demo" - name: Creating Email Notification with the list of names of subscribed events in the system. cisco.dnac.events_and_notifications_workflow_manager: @@ -763,10 +763,10 @@ state: merged config: - email_event_notification: - notification_name: "Email Notification" + name: "Email Notification" description: "Notification description for email subscription creation" - events_name_list: ["AP Flap", "AP Reboot Crash"] site_name_list: ["Global/India", "Global/USA"] + events_name_list: ["AP Flap", "AP Reboot Crash"] from_email_address: "catalyst@cisco.com" to_email_addresses: ["test@cisco.com", "demo@cisco.com"] subject: "Mail test" @@ -786,10 +786,10 @@ state: merged config: - email_event_notification: - notification_name: "Email Notification" + name: "Email Notification" description: "Notification description for email subscription updation" - events_name_list: ["AP Flap", "AP Reboot Crash"] site_name_list: ["Global/India", "Global/USA"] + events_name_list: ["AP Flap", "AP Reboot Crash"] from_email_address: "catalyst@cisco.com" to_email_addresses: ["test@cisco.com", "demo@cisco.com", "update@cisco.com"] subject: "Mail test for updation" @@ -809,11 +809,11 @@ state: merged config: - syslog_event_notification: - notification_name: "Syslog Notification." + name: "Syslog Notification." description: "Notification for syslog events subscription" - syslog_dest_name: "Syslog Demo" - events_name_list: ["AP Flap", "AP Reboot Crash"] site_name_list: ["Global/India", "Global/USA"] + events_name_list: ["AP Flap", "AP Reboot Crash"] + syslog_dest_name: "Syslog Demo" - name: Updating Syslog Notification with the list of names of subscribed events in the system. cisco.dnac.events_and_notifications_workflow_manager: @@ -829,10 +829,10 @@ state: merged config: - syslog_event_notification: - notification_name: "Syslog Notification." + name: "Syslog Notification." description: "Updated notification for syslog events subscription" - events_name_list: ["AP Flap", "AP Reboot Crash"] site_name_list: ["Global/India", "Global/USA", "Global/China"] + events_name_list: ["AP Flap", "AP Reboot Crash"] - name: Deleting ITSM Integration Setting with given name from the system. cisco.dnac.events_and_notifications_workflow_manager: @@ -864,7 +864,7 @@ state: deleted config: - webhook_event_notification: - notification_name: "Webhook Notification" + name: "Webhook Notification" - name: Deleting Email Events Subscription Notification with given name from the system. cisco.dnac.events_and_notifications_workflow_manager: @@ -880,7 +880,7 @@ state: deleted config: - email_event_notification: - notification_name: "Email Notification" + name: "Email Notification" - name: Deleting Syslog Events Subscription Notification with given name from the system. cisco.dnac.events_and_notifications_workflow_manager: @@ -896,7 +896,7 @@ state: deleted config: - syslog_event_notification: - notification_name: "Syslog Notification" + name: "Syslog Notification" """ @@ -1026,52 +1026,52 @@ def validate_input(self): }, 'webhook_event_notification': { 'type': 'dict', - 'notification_name': {'type': 'str'}, + 'name': {'type': 'str'}, 'version': {'type': 'str'}, 'description': {'type': 'str'}, - 'webhook_dest_name': {'type': 'str'}, + 'site_name_list': {'type': 'list', 'elements': 'str'}, 'events_name_list': {'type': 'list', 'elements': 'str'}, + 'webhook_dest_name': {'type': 'str'}, 'domain': {'type': 'str'}, 'sub_domains': {'type': 'list', 'elements': 'str'}, 'event_types': {'type': 'list', 'elements': 'str'}, 'event_categories': {'type': 'list', 'elements': 'str'}, 'event_severities': {'type': 'list', 'elements': 'str'}, 'event_sources': {'type': 'list', 'elements': 'str'}, - 'site_name_list': {'type': 'list', 'elements': 'str'}, }, 'email_event_notification': { 'type': 'dict', - 'notification_name': {'type': 'str'}, + 'name': {'type': 'str'}, 'version': {'type': 'str'}, 'description': {'type': 'str'}, + 'site_name_list': {'type': 'list', 'elements': 'str'}, + 'events_name_list': {'type': 'list', 'elements': 'str'}, 'from_email_address': {'type': 'str'}, 'to_email_addresses': {'type': 'list', 'elements': 'str'}, 'subject': {'type': 'str'}, 'instance_name': {'type': 'str'}, 'instance_description': {'type': 'str'}, - 'events_name_list': {'type': 'list', 'elements': 'str'}, 'domain': {'type': 'str'}, 'sub_domains': {'type': 'list', 'elements': 'str'}, 'event_types': {'type': 'list', 'elements': 'str'}, 'event_categories': {'type': 'list', 'elements': 'str'}, 'event_severities': {'type': 'list', 'elements': 'str'}, 'event_sources': {'type': 'list', 'elements': 'str'}, - 'site_name_list': {'type': 'list', 'elements': 'str'}, }, 'syslog_event_notification': { 'type': 'dict', - 'notification_name': {'type': 'str'}, + 'name': {'type': 'str'}, 'version': {'type': 'str'}, 'description': {'type': 'str'}, - 'syslog_dest_name': {'type': 'str'}, + 'site_name_list': {'type': 'list', 'elements': 'str'}, 'events_name_list': {'type': 'list', 'elements': 'str'}, + 'syslog_dest_name': {'type': 'str'}, 'domain': {'type': 'str'}, 'sub_domains': {'type': 'list', 'elements': 'str'}, 'event_types': {'type': 'list', 'elements': 'str'}, 'event_categories': {'type': 'list', 'elements': 'str'}, 'event_severities': {'type': 'list', 'elements': 'str'}, 'event_sources': {'type': 'list', 'elements': 'str'}, - 'site_name_list': {'type': 'list', 'elements': 'str'}, }, } @@ -2761,7 +2761,7 @@ def collect_syslog_notification_playbook_params(self, syslog_notification_detail """ syslog_notification_params = [] - name = syslog_notification_details.get('notification_name') + name = syslog_notification_details.get('name') playbook_params = { 'name': name, 'description': syslog_notification_details.get('description'), @@ -2864,7 +2864,7 @@ def mandatory_syslog_notification_parameter_check(self, syslog_notification_para description = syslog_notification_params.get("description") if not notification_name: - required_params_absent.append("notification_name") + required_params_absent.append("name") if not description: required_params_absent.append("description") @@ -3194,7 +3194,6 @@ def get_webhook_notification_details(self): Retrieves the details of a Webhook Event Notification subscription from the Cisco Catalyst Center. Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - notification_name (str): The name of the Webhook Event Notification subscription. Returns: dict or None: A dictionary containing the details of the Webhook Event Notification subscription if found. Returns None if no subscription is found or if an error occurs during the API call. @@ -3283,7 +3282,7 @@ def collect_webhook_notification_playbook_params(self, webhook_notification_deta """ webhook_notification_params = [] - name = webhook_notification_details.get('notification_name') + name = webhook_notification_details.get('name') playbook_params = { 'name': name, 'description': webhook_notification_details.get('description'), @@ -3386,7 +3385,7 @@ def mandatory_webhook_notification_parameter_check(self, webhook_notification_pa description = webhook_notification_params.get("description") if not notification_name: - required_params_absent.append("notification_name") + required_params_absent.append("name") if not description: required_params_absent.append("description") @@ -3609,7 +3608,6 @@ def get_email_notification_details(self): Retrieves the details of a email Event Notification subscription from the Cisco Catalyst Center. Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - notification_name (str): The name of the email Event Notification subscription/ Returns: dict or None: A dictionary containing the details of the email Event Notification subscription if found. Returns None if no subscription is found or if an error occurs during the API call. @@ -3697,7 +3695,7 @@ def collect_email_notification_playbook_params(self, email_notification_details) """ email_notification_params = [] - email_notf_name = email_notification_details.get('notification_name') + email_notf_name = email_notification_details.get('name') playbook_params = { 'name': email_notf_name, 'description': email_notification_details.get('description'), @@ -3818,7 +3816,7 @@ def mandatory_email_notification_parameter_check(self, email_notification_params description = email_notification_params.get("description") if not notification_name: - required_params_absent.append("notification_name") + required_params_absent.append("name") if not description: required_params_absent.append("description") @@ -4498,7 +4496,7 @@ def get_diff_merged(self, config): # Create Rest Webhook Events Subscription Notification in Cisco Catalyst Center if config.get('webhook_event_notification'): webhook_notification_details = self.want.get('webhook_event_notification') - notification_name = webhook_notification_details.get('notification_name') + notification_name = webhook_notification_details.get('name') if not notification_name: self.status = "failed" @@ -4539,7 +4537,7 @@ def get_diff_merged(self, config): # Create Email Events Subscription Notification in Cisco Catalyst Center if config.get('email_event_notification'): email_notification_details = self.want.get('email_event_notification') - notification_name = email_notification_details.get('notification_name') + notification_name = email_notification_details.get('name') if not notification_name: self.status = "failed" @@ -4581,7 +4579,7 @@ def get_diff_merged(self, config): # Create Syslog Events Subscription Notification in Cisco Catalyst Center if config.get('syslog_event_notification'): syslog_notification_details = self.want.get('syslog_event_notification') - notification_name = syslog_notification_details.get('notification_name') + notification_name = syslog_notification_details.get('name') if not notification_name: self.status = "failed" @@ -4695,7 +4693,7 @@ def get_diff_deleted(self, config): # Delete Webhook Events Subscription Notification from Cisco Catalyst Center if config.get('webhook_event_notification'): webhook_notification_details = self.want.get('webhook_event_notification') - webhook_notification_name = webhook_notification_details.get('notification_name') + webhook_notification_name = webhook_notification_details.get('name') current_webhook_notifications = self.have.get("webhook_subscription_notifications") webhook_notification_id = None @@ -4724,7 +4722,7 @@ def get_diff_deleted(self, config): # Delete Email Events Subscription Notification from Cisco Catalyst Center if config.get('email_event_notification'): email_notification_details = self.want.get('email_event_notification') - email_notification_name = email_notification_details.get('notification_name') + email_notification_name = email_notification_details.get('name') current_email_notifications = self.have.get("email_subscription_notifications") email_notification_id = None @@ -4753,7 +4751,7 @@ def get_diff_deleted(self, config): # Delete Syslog Events Subscription Notification from Cisco Catalyst Center if config.get('syslog_event_notification'): syslog_notification_details = self.want.get('syslog_event_notification') - syslog_notification_name = syslog_notification_details.get('notification_name') + syslog_notification_name = syslog_notification_details.get('name') current_syslog_notifications = self.have.get("syslog_subscription_notifications") syslog_notification_id = None @@ -4893,7 +4891,7 @@ def verify_diff_merged(self, config): if config.get('webhook_event_notification'): webhook_notification_details = self.want.get('webhook_event_notification') - web_notification_name = webhook_notification_details.get('notification_name') + web_notification_name = webhook_notification_details.get('name') current_webhook_notifications = self.have.get("webhook_subscription_notifications") is_webhook_notification_exist = False @@ -4914,7 +4912,7 @@ def verify_diff_merged(self, config): if config.get('email_event_notification'): email_notification_details = self.want.get('email_event_notification') - email_notification_name = email_notification_details.get('notification_name') + email_notification_name = email_notification_details.get('name') current_email_notifications = self.have.get("email_subscription_notifications") is_email_notification_exist = False @@ -4935,7 +4933,7 @@ def verify_diff_merged(self, config): if config.get('syslog_event_notification'): syslog_notification_details = self.want.get('syslog_event_notification') - syslog_notification_name = syslog_notification_details.get('notification_name') + syslog_notification_name = syslog_notification_details.get('name') current_syslog_notifications = self.have.get("syslog_subscription_notifications") is_syslog_notification_exist = False @@ -4999,7 +4997,7 @@ def verify_diff_deleted(self, config): if config.get('webhook_event_notification'): webhook_notification_details = self.want.get('webhook_event_notification') - web_notification_name = webhook_notification_details.get('notification_name') + web_notification_name = webhook_notification_details.get('name') current_webhook_notifications = self.have.get("webhook_subscription_notifications") is_webhook_notification_deleted = True @@ -5020,7 +5018,7 @@ def verify_diff_deleted(self, config): if config.get('email_event_notification'): email_notification_details = self.want.get('email_event_notification') - email_notification_name = email_notification_details.get('notification_name') + email_notification_name = email_notification_details.get('name') current_email_notifications = self.have.get("email_subscription_notifications") is_email_notification_deleted = True @@ -5041,7 +5039,7 @@ def verify_diff_deleted(self, config): if config.get('syslog_event_notification'): syslog_notification_details = self.want.get('syslog_event_notification') - syslog_notification_name = syslog_notification_details.get('notification_name') + syslog_notification_name = syslog_notification_details.get('name') current_syslog_notifications = self.have.get("syslog_subscription_notifications") is_syslog_notification_deleted = True From d64cc272112e9a769b5616097aa37c46203292d9 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 4 Jun 2024 09:35:05 +0530 Subject: [PATCH 61/78] Removed the params which cannot be updated from the example playbook --- plugins/modules/ise_radius_integration_workflow_manager.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index acf475a0ff..b8c576a6cd 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -312,11 +312,8 @@ server_type: AAA server_ip_address: 10.0.0.1 protocol: RADIUS_TACACS - authentication_port: 1812 - accounting_port: 1813 retries: 3 timeout: 5 - role: secondary - name: Update an Cisco ISE server. cisco.dnac.ise_radius_integration_workflow_manager: @@ -336,11 +333,8 @@ server_type: ISE server_ip_address: 10.0.0.2 protocol: RADIUS_TACACS - authentication_port: 1812 - accounting_port: 1813 retries: 3 timeout: 5 - role: primary use_dnac_cert_for_pxgrid: False pxgrid_enabled: True cisco_ise_dtos: From 55c391e7524fbcf89439982ae501eee16a04257a Mon Sep 17 00:00:00 2001 From: Rugvedi Kapse Date: Tue, 4 Jun 2024 13:51:14 -0700 Subject: [PATCH 62/78] compliance bug fix - modified error msg --- .../modules/network_compliance_workflow_manager.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/modules/network_compliance_workflow_manager.py b/plugins/modules/network_compliance_workflow_manager.py index 460cac5f87..e696231d31 100644 --- a/plugins/modules/network_compliance_workflow_manager.py +++ b/plugins/modules/network_compliance_workflow_manager.py @@ -607,6 +607,10 @@ def get_device_ids_from_ip(self, ip_address_list): except Exception as e: # Log an error message if any exception occurs during the process self.log("Error while fetching device ID for device: '{0}' from Cisco Catalyst Center: {1}".format(device_ip, str(e)), "ERROR") + if not mgmt_ip_instance_id_map: + self.msg = "Reachable devices not found in the IP Address List: {0}".format(ip_address_list) + self.update_result("ok", False, self.msg, "INFO") + self.module.exit_json(**self.result) return mgmt_ip_instance_id_map @@ -671,6 +675,11 @@ def get_device_ids_from_site(self, site_name, site_id): # Log an error message if any exception occurs during the process self.log("Unable to fetch the device(s) associated to the site '{0}' due to {1}".format(site_name, str(e)), "ERROR") + if not mgmt_ip_instance_id_map: + self.msg = "Reachable devices not found at Site: {0}".format(site_name) + self.update_result("ok", False, self.msg, "INFO") + self.module.exit_json(**self.result) + return mgmt_ip_instance_id_map def get_device_id_list(self, ip_address_list, site_name): @@ -834,7 +843,7 @@ def get_want(self, config): required, self.msg, categorized_devices = self.is_sync_required(compliance_details, mgmt_ip_instance_id_map) self.log("Is Sync Requied: {0} {1}".format(required, self.msg), "DEBUG") if not required: - self.update_result("success", False, self.msg, "INFO") + self.update_result("ok", False, self.msg, "INFO") self.module.exit_json(**self.result) # Get the device IDs of devices in the "OTHER" category and "COMPLIANT" category From 6e2bc814e46d9df91c08968fee653bb1b1451aff Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 5 Jun 2024 10:25:47 +0530 Subject: [PATCH 63/78] Delete all the config even a error occurs in the middle --- .../device_credential_workflow_manager.py | 69 +++++++++++++++---- 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/plugins/modules/device_credential_workflow_manager.py b/plugins/modules/device_credential_workflow_manager.py index 6445f0671f..88b925f59b 100644 --- a/plugins/modules/device_credential_workflow_manager.py +++ b/plugins/modules/device_credential_workflow_manager.py @@ -2408,20 +2408,27 @@ def delete_device_credential(self, config): "httpsRead": "https_read", "httpsWrite": "https_write" } + failed_status = False + changed_status = False for item in have_values: - config_itr = 0 + config_itr = -1 final_response.update({item: []}) for value in have_values.get(item): + config_itr = config_itr + 1 + description = config.get("global_credential_details") \ + .get(credential_mapping.get(item))[config_itr].get("description") if value is None: self.log("Credential Name: {0}".format(item), "DEBUG") self.log("Credential Item: {0}".format(config.get("global_credential_details") .get(credential_mapping.get(item))), "DEBUG") - final_response.get(item).append( - str(config.get("global_credential_details") - .get(credential_mapping.get(item))[config_itr]) + " is not found." - ) + final_response.get(item).append({ + "description": description, + "response": "Global credential not found" + }) continue + _id = have_values.get(item)[config_itr].get("id") + changed_status = True response = self.dnac._exec( family="discovery", function="delete_global_credential_v2", @@ -2431,21 +2438,59 @@ def delete_device_credential(self, config): self.log("Received API response for 'delete_global_credential_v2': {0}" .format(response), "DEBUG") validation_string = "global credential deleted successfully" - self.check_task_response_status(response, validation_string).check_return_status() - final_response.get(item).append(_id) - config_itr = config_itr + 1 + response = response.get("response") + if response.get("errorcode") is not None: + self.msg = response.get("response").get("detail") + self.status = "failed" + return self + + task_id = response.get("taskId") + while True: + task_details = self.get_task_details(task_id) + self.log('Getting task details from task ID {0}: {1}'.format(task_id, task_details), "DEBUG") + + if task_details.get("isError") is True: + if task_details.get("failureReason"): + failure_msg = str(task_details.get("failureReason")) + else: + failure_msg = str(task_details.get("progress")) + self.status = "failed" + break + + if validation_string in task_details.get("progress").lower(): + self.status = "success" + break + + self.log("progress set to {0} for taskid: {1}".format(task_details.get('progress'), task_id), "DEBUG") + + if self.status == "failed": + failed_status = True + final_response.get(item).append({ + "description": description, + "failure_response": failure_msg + }) + else: + final_response.get(item).append({ + "description": description, + "response": "Global credential deleted successfully" + }) self.log("Deleting device credential API input parameters: {0}" .format(final_response), "DEBUG") - self.log("Successfully deleted global device credential.", "INFO") result_global_credential.update({ "Deletion": { "response": final_response, - "msg": "Global Device Credentials Deleted Successfully" } }) - self.msg = "Global Device Credentials Updated Successfully" - self.status = "success" + if failed_status is True: + self.msg = "Global device credentials are not deleted." + self.module.fail_json(msg=self.msg, response=final_response) + else: + self.result['changed'] = changed_status + self.msg = "Global device credentials deleted successfully" + self.log(str(self.msg), "INFO") + self.status = "success" + return self def get_diff_deleted(self, config): From eb99e4d5c1f1f13cc043ed078f740cb1b4d93bbe Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 5 Jun 2024 14:18:16 +0530 Subject: [PATCH 64/78] Verifying the existence of servers in the network_aaa and clientandEndpoint_aaa --- .../network_settings_workflow_manager.py | 112 +++++++++++++++--- 1 file changed, 94 insertions(+), 18 deletions(-) diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index b52cb7cdb1..ab12b7767f 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -882,6 +882,46 @@ def get_obj_params(self, get_object): return obj_params + def is_server_exists(self, ip_address): + """ + Finds if the provided Authentication and Policy Server with + the ip_address is available in the system or not. + + Parameters: + ip_address (str) - IP Address of the Authentication and Policy Server. + + Returns: + True or False - True if the Authentication and Policy Server is + available in the system. Else, False. + """ + + try: + response = self.dnac._exec( + family="system_settings", + function='get_authentication_and_policy_servers' + ) + self.log("Received API response from 'get_authentication_and_policy_servers': {0}" + .format(response), "DEBUG") + if not response: + self.msg = "Failed to retrieve the Authentication and Policy Server details" + self.log(str(self.msg), "ERROR") + self.status = "failed" + return self.check_return_status() + + response = response.get("response") + ise_server_details = get_dict_result(response, "ipAddress", ip_address) + if not ise_server_details: + return False + + self.log("Server details for the IP address '{0}': {1}".format(ip_address, ise_server_details), "DEBUG") + except Exception as msg: + self.msg = "Exception occurred while retrieving site_id from the site_name: {0}".format(msg) + self.log(str(self.msg), "CRITICAL") + self.status = "failed" + return self.check_return_status() + + return True + def get_site_id(self, site_name): """ Get the site id from the site name. @@ -1954,9 +1994,15 @@ def get_want_network(self, network_management_details): primary_server_address = network_aaa.get("primary_server_address") if primary_server_address: - want_network_settings.get("network_aaa").update({ - "network": primary_server_address - }) + if self.is_server_exists(primary_server_address): + want_network_settings.get("network_aaa").update({ + "network": primary_server_address + }) + else: + self.msg = "The 'primary_server_address' - '{0}' under 'network_aaa' is not found in the system." \ + .format(primary_server_address) + self.status = "failed" + return self else: self.msg = "Missing required parameter 'primary_server_address' in network_aaa." self.status = "failed" @@ -1965,9 +2011,15 @@ def get_want_network(self, network_management_details): if server_type == "ISE": pan_address = network_aaa.get("pan_address") if pan_address: - want_network_settings.get("network_aaa").update({ - "ipAddress": pan_address - }) + if self.is_server_exists(pan_address): + want_network_settings.get("network_aaa").update({ + "ipAddress": pan_address + }) + else: + self.msg = "The 'pan_address' - '{0}' under 'network_aaa' is not found in the system." \ + .format(pan_address) + self.status = "failed" + return self else: self.msg = "Missing required parameter 'pan_address' for ISE server in network_aaa." self.status = "failed" @@ -1975,9 +2027,15 @@ def get_want_network(self, network_management_details): else: secondary_server_address = network_aaa.get("secondary_server_address") if secondary_server_address: - want_network_settings.get("network_aaa").update({ - "ipAddress": secondary_server_address - }) + if self.is_server_exists(secondary_server_address): + want_network_settings.get("network_aaa").update({ + "ipAddress": secondary_server_address + }) + else: + self.msg = "The 'secondary_server_address' - '{0}' under 'network_aaa' is not found in the system." \ + .format(secondary_server_address) + self.status = "failed" + return self protocol = network_aaa.get("protocol") if protocol: @@ -2021,9 +2079,15 @@ def get_want_network(self, network_management_details): primary_server_address = clientAndEndpoint_aaa.get("primary_server_address") if primary_server_address: - want_network_settings.get("clientAndEndpoint_aaa").update({ - "network": primary_server_address - }) + if self.is_server_exists(primary_server_address): + want_network_settings.get("clientAndEndpoint_aaa").update({ + "network": primary_server_address + }) + else: + self.msg = "The 'primary_server_address' - '{0}' under 'clientAndEndpoint_aaa' is not found in the system." \ + .format(primary_server_address) + self.status = "failed" + return self else: self.msg = "Missing required parameter 'primary_server_address' in client_and_endpoint_aaa." self.status = "failed" @@ -2032,9 +2096,15 @@ def get_want_network(self, network_management_details): if server_type == "ISE": pan_address = clientAndEndpoint_aaa.get("pan_address") if pan_address: - want_network_settings.get("clientAndEndpoint_aaa").update({ - "ipAddress": pan_address - }) + if self.is_server_exists(pan_address): + want_network_settings.get("clientAndEndpoint_aaa").update({ + "ipAddress": pan_address + }) + else: + self.msg = "The 'pan_address' - '{0}' under 'clientAndEndpoint_aaa' is not found in the system." \ + .format(pan_address) + self.status = "failed" + return self else: self.msg = "Missing required parameter 'pan_address' for ISE server in client_and_endpoint_aaa." self.status = "failed" @@ -2042,9 +2112,15 @@ def get_want_network(self, network_management_details): else: secondary_server_address = clientAndEndpoint_aaa.get("secondary_server_address") if secondary_server_address: - want_network_settings.get("clientAndEndpoint_aaa").update({ - "ipAddress": secondary_server_address - }) + if self.is_server_exists(secondary_server_address): + want_network_settings.get("clientAndEndpoint_aaa").update({ + "ipAddress": secondary_server_address + }) + else: + self.msg = "The 'secondary_server_address' - '{0}' under 'clientAndEndpoint_aaa' is not found in the system." \ + .format(secondary_server_address) + self.status = "failed" + return self protocol = clientAndEndpoint_aaa.get("protocol") if protocol: From 44abdf456a64bf0f1eba28727b3cfca765b1da6b Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 5 Jun 2024 18:42:41 +0530 Subject: [PATCH 65/78] Addressed the review comments --- .../network_settings_workflow_manager.py | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index ab12b7767f..6b448f987c 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -909,13 +909,15 @@ def is_server_exists(self, ip_address): return self.check_return_status() response = response.get("response") - ise_server_details = get_dict_result(response, "ipAddress", ip_address) - if not ise_server_details: + server_details = get_dict_result(response, "ipAddress", ip_address) + if not server_details: + self.log("The server with IP Address '{0}' is not available in the system.".format(ip_address)) return False - self.log("Server details for the IP address '{0}': {1}".format(ip_address, ise_server_details), "DEBUG") + self.log("Server details for the IP address '{0}': {1}".format(ip_address, server_details), "DEBUG") except Exception as msg: - self.msg = "Exception occurred while retrieving site_id from the site_name: {0}".format(msg) + self.msg = "Exception occurred while retrieving server details from the IP Address '{0}': {1}" \ + .format(ip_address, msg) self.log(str(self.msg), "CRITICAL") self.status = "failed" return self.check_return_status() @@ -1994,15 +1996,15 @@ def get_want_network(self, network_management_details): primary_server_address = network_aaa.get("primary_server_address") if primary_server_address: - if self.is_server_exists(primary_server_address): - want_network_settings.get("network_aaa").update({ - "network": primary_server_address - }) - else: + if not self.is_server_exists(primary_server_address): self.msg = "The 'primary_server_address' - '{0}' under 'network_aaa' is not found in the system." \ .format(primary_server_address) self.status = "failed" return self + + want_network_settings.get("network_aaa").update({ + "network": primary_server_address + }) else: self.msg = "Missing required parameter 'primary_server_address' in network_aaa." self.status = "failed" @@ -2011,15 +2013,15 @@ def get_want_network(self, network_management_details): if server_type == "ISE": pan_address = network_aaa.get("pan_address") if pan_address: - if self.is_server_exists(pan_address): - want_network_settings.get("network_aaa").update({ - "ipAddress": pan_address - }) - else: + if not self.is_server_exists(pan_address): self.msg = "The 'pan_address' - '{0}' under 'network_aaa' is not found in the system." \ .format(pan_address) self.status = "failed" return self + + want_network_settings.get("network_aaa").update({ + "ipAddress": pan_address + }) else: self.msg = "Missing required parameter 'pan_address' for ISE server in network_aaa." self.status = "failed" @@ -2027,16 +2029,16 @@ def get_want_network(self, network_management_details): else: secondary_server_address = network_aaa.get("secondary_server_address") if secondary_server_address: - if self.is_server_exists(secondary_server_address): - want_network_settings.get("network_aaa").update({ - "ipAddress": secondary_server_address - }) - else: + if not self.is_server_exists(secondary_server_address): self.msg = "The 'secondary_server_address' - '{0}' under 'network_aaa' is not found in the system." \ .format(secondary_server_address) self.status = "failed" return self + want_network_settings.get("network_aaa").update({ + "ipAddress": secondary_server_address + }) + protocol = network_aaa.get("protocol") if protocol: want_network_settings.get("network_aaa").update({ @@ -2079,15 +2081,15 @@ def get_want_network(self, network_management_details): primary_server_address = clientAndEndpoint_aaa.get("primary_server_address") if primary_server_address: - if self.is_server_exists(primary_server_address): - want_network_settings.get("clientAndEndpoint_aaa").update({ - "network": primary_server_address - }) - else: + if not self.is_server_exists(primary_server_address): self.msg = "The 'primary_server_address' - '{0}' under 'clientAndEndpoint_aaa' is not found in the system." \ .format(primary_server_address) self.status = "failed" return self + + want_network_settings.get("clientAndEndpoint_aaa").update({ + "network": primary_server_address + }) else: self.msg = "Missing required parameter 'primary_server_address' in client_and_endpoint_aaa." self.status = "failed" @@ -2096,15 +2098,15 @@ def get_want_network(self, network_management_details): if server_type == "ISE": pan_address = clientAndEndpoint_aaa.get("pan_address") if pan_address: - if self.is_server_exists(pan_address): - want_network_settings.get("clientAndEndpoint_aaa").update({ - "ipAddress": pan_address - }) - else: + if not self.is_server_exists(pan_address): self.msg = "The 'pan_address' - '{0}' under 'clientAndEndpoint_aaa' is not found in the system." \ .format(pan_address) self.status = "failed" return self + + want_network_settings.get("clientAndEndpoint_aaa").update({ + "ipAddress": pan_address + }) else: self.msg = "Missing required parameter 'pan_address' for ISE server in client_and_endpoint_aaa." self.status = "failed" @@ -2112,16 +2114,16 @@ def get_want_network(self, network_management_details): else: secondary_server_address = clientAndEndpoint_aaa.get("secondary_server_address") if secondary_server_address: - if self.is_server_exists(secondary_server_address): - want_network_settings.get("clientAndEndpoint_aaa").update({ - "ipAddress": secondary_server_address - }) - else: + if not self.is_server_exists(secondary_server_address): self.msg = "The 'secondary_server_address' - '{0}' under 'clientAndEndpoint_aaa' is not found in the system." \ .format(secondary_server_address) self.status = "failed" return self + want_network_settings.get("clientAndEndpoint_aaa").update({ + "ipAddress": secondary_server_address + }) + protocol = clientAndEndpoint_aaa.get("protocol") if protocol: want_network_settings.get("clientAndEndpoint_aaa").update({ From ee12baa81df51eeba919f4c428459fa74b19c508 Mon Sep 17 00:00:00 2001 From: Madhan Date: Wed, 5 Jun 2024 23:24:26 +0530 Subject: [PATCH 66/78] update provisioning attribute --- plugins/modules/provision_workflow_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/provision_workflow_manager.py b/plugins/modules/provision_workflow_manager.py index 274babdde2..fd72ec785f 100644 --- a/plugins/modules/provision_workflow_manager.py +++ b/plugins/modules/provision_workflow_manager.py @@ -105,8 +105,8 @@ post /dna/intent/api/v1/business/sda/provision-device post /dna/intent/api/v1/wireless/provision - - Added 'provisioning' option in v6.14.1 - - Added provisioning and reprovisioning of wireless devices in v6.14.1 + - Added 'provisioning' option in v6.15.1 + - Added provisioning and reprovisioning of wireless devices in v6.15.1 """ From a189d7d9d717897f1307b54f7f0b22254ee41ae7 Mon Sep 17 00:00:00 2001 From: Madhan Date: Wed, 5 Jun 2024 23:28:15 +0530 Subject: [PATCH 67/78] updated changelogs --- changelogs/changelog.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 74117ed9e4..0047be7d7c 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -916,3 +916,17 @@ releases: release_summary: Fix module name. minor_changes: - Fix module name from network_device_config__info to configuration_archive_details_info. + 6.15.1: + release_date: "2024-06-05" + changes: + release_summary: Code changes in workflow manager modules. + minor_changes: + - Added example playbooks in device_provision_workflow.yml + - Added API to validate the server address + - Minor bug fixes in device_credential_workflow_manager.py module + - Checking SNMP versions in events_and_notifications_workflow_manager.py module + - Added new attribute 'ise_integration_wait_time' in ise_radius_integration_workflow_manager.py + - Added example playbooks in network_compliance_workflow_manager.py + - Added detailed documentation in network_settings_workflow_manager.py + - Added new attribute 'provisioning' in provision_workflow_manager.py + - Added new attributes 'choices', 'failure_policy' in template_workflow_manager.py From f74d5098f4e4c70b37f138b9b15b096eff7d3904 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Thu, 6 Jun 2024 15:27:42 +0530 Subject: [PATCH 68/78] Resolved the problem after bringing the ISE server to 'Failed' state --- ...ise_radius_integration_workflow_manager.py | 60 +++++++++++++++---- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index b57c0a3eef..973cfcec1e 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -656,7 +656,7 @@ def auth_server_exists(self, ipAddress): AuthServer.update({"id": auth_server_details.get("instanceUuid")}) AuthServer["details"] = self.get_auth_server_params(auth_server_details) - self.log("Formatted global pool details: {0}".format(AuthServer), "DEBUG") + self.log("Formatted Authenticaion and Policy Server details: {0}".format(AuthServer), "DEBUG") return AuthServer def get_have_authentication_policy_server(self, config): @@ -948,9 +948,12 @@ def get_want_authentication_policy_server(self, auth_policy_server): auth_server.get("ciscoIseDtos").append({}) user_name = ise_credential.get("user_name") if not user_name: - self.msg = "Missing parameter 'user_name' is required when server_type is ISE." - self.status = "failed" - return self + if not auth_server_exists: + self.msg = "Missing parameter 'user_name' is required when server_type is ISE." + self.status = "failed" + return self + else: + user_name = auth_server_details.get("ciscoIseDtos")[0].get("userName") auth_server.get("ciscoIseDtos")[position_ise_creds].update({ "userName": user_name @@ -973,9 +976,12 @@ def get_want_authentication_policy_server(self, auth_policy_server): fqdn = ise_credential.get("fqdn") if not fqdn: - self.msg = "Missing parameter 'fqdn' is required when server_type is ISE." - self.status = "failed" - return self + if not auth_server_exists: + self.msg = "Missing parameter 'fqdn' is required when server_type is ISE." + self.status = "failed" + return self + else: + fqdn = auth_server_details.get("ciscoIseDtos")[0].get("fqdn") auth_server.get("ciscoIseDtos")[position_ise_creds].update({"fqdn": fqdn}) @@ -1222,13 +1228,41 @@ def update_auth_policy_server(self, ipAddress): function="add_authentication_and_policy_server_access_configuration", params=auth_server_params, ) - if not is_ise_server: - validation_string = "successfully created aaa settings" - else: - validation_string = "operation sucessful" + validation_string_set = ("successfully created aaa settings", "operation sucessful") + response = response.get("response") + if response.get("errorcode") is not None: + self.msg = response.get("response").get("detail") + self.status = "failed" + return self + + task_id = response.get("taskId") + is_certificate_required = False + while True: + task_details = self.get_task_details(task_id) + self.log('Getting task details from task ID {0}: {1}'.format(task_id, task_details), "DEBUG") + + if task_details.get("isError") is True: + if task_details.get("failureReason"): + self.msg = str(task_details.get("failureReason")) + else: + self.msg = str(task_details.get("progress")) + self.status = "failed" + break + + for validation_string in validation_string_set: + if validation_string in task_details.get("progress").lower(): + self.result['changed'] = True + if validation_string == "operation sucessful": + is_certificate_required = True + self.status = "success" + + if self.result['changed'] is True: + self.log("The task with task id '{0}' is successfully executed".format(task_id), "DEBUG") + break + + self.log("progress set to {0} for taskid: {1}".format(task_details.get('progress'), task_id), "DEBUG") - self.check_task_response_status(response, validation_string).check_return_status() - if is_ise_server: + if is_ise_server and is_certificate_required: trusted_server = self.want.get("trusted_server") self.accept_cisco_ise_server_certificate(ipAddress, trusted_server) ise_integration_wait_time = self.want.get("ise_integration_wait_time") From b305749ce9d6963af65c9bf0101877cdf0dbc228 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Thu, 6 Jun 2024 15:46:42 +0530 Subject: [PATCH 69/78] Addressed the review comments --- .../ise_radius_integration_workflow_manager.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index 973cfcec1e..bf765a0ae5 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -952,8 +952,8 @@ def get_want_authentication_policy_server(self, auth_policy_server): self.msg = "Missing parameter 'user_name' is required when server_type is ISE." self.status = "failed" return self - else: - user_name = auth_server_details.get("ciscoIseDtos")[0].get("userName") + + user_name = auth_server_details.get("ciscoIseDtos")[0].get("userName") auth_server.get("ciscoIseDtos")[position_ise_creds].update({ "userName": user_name @@ -980,8 +980,8 @@ def get_want_authentication_policy_server(self, auth_policy_server): self.msg = "Missing parameter 'fqdn' is required when server_type is ISE." self.status = "failed" return self - else: - fqdn = auth_server_details.get("ciscoIseDtos")[0].get("fqdn") + + fqdn = auth_server_details.get("ciscoIseDtos")[0].get("fqdn") auth_server.get("ciscoIseDtos")[position_ise_creds].update({"fqdn": fqdn}) @@ -1240,10 +1240,10 @@ def update_auth_policy_server(self, ipAddress): while True: task_details = self.get_task_details(task_id) self.log('Getting task details from task ID {0}: {1}'.format(task_id, task_details), "DEBUG") - if task_details.get("isError") is True: - if task_details.get("failureReason"): - self.msg = str(task_details.get("failureReason")) + failure_reason = task_details.get("failureReason") + if failure_reason: + self.msg = str(failure_reason) else: self.msg = str(task_details.get("progress")) self.status = "failed" @@ -1260,7 +1260,7 @@ def update_auth_policy_server(self, ipAddress): self.log("The task with task id '{0}' is successfully executed".format(task_id), "DEBUG") break - self.log("progress set to {0} for taskid: {1}".format(task_details.get('progress'), task_id), "DEBUG") + self.log("Progress set to {0} for taskid: {1}".format(task_details.get('progress'), task_id), "DEBUG") if is_ise_server and is_certificate_required: trusted_server = self.want.get("trusted_server") From af703d64b140f013cb6810eb70ee42f23feb58f2 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Thu, 6 Jun 2024 15:58:22 +0530 Subject: [PATCH 70/78] Addressed the review comments --- plugins/modules/ise_radius_integration_workflow_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index bf765a0ae5..d795d78fa1 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -1231,7 +1231,7 @@ def update_auth_policy_server(self, ipAddress): validation_string_set = ("successfully created aaa settings", "operation sucessful") response = response.get("response") if response.get("errorcode") is not None: - self.msg = response.get("response").get("detail") + self.msg = response.get("detail") self.status = "failed" return self From 9a4d49c0b186a15252ce242ec14743a90f40dd0e Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 6 Jun 2024 20:18:22 +0530 Subject: [PATCH 71/78] Address review comments and added email validation check while creating email events subscription notification. --- ...nts_and_notifications_workflow_manager.yml | 26 +- plugins/module_utils/dnac.py | 24 + ...ents_and_notifications_workflow_manager.py | 840 ++++++++++-------- 3 files changed, 483 insertions(+), 407 deletions(-) diff --git a/playbooks/events_and_notifications_workflow_manager.yml b/playbooks/events_and_notifications_workflow_manager.yml index 8d38c0fd5a..efd32b883e 100644 --- a/playbooks/events_and_notifications_workflow_manager.yml +++ b/playbooks/events_and_notifications_workflow_manager.yml @@ -61,27 +61,27 @@ username: "{{item.itsm_setting.connection_settings.username}}" password: "{{item.itsm_setting.connection_settings.password}}" webhook_event_notification: - notification_name: "{{item.webhook_event_notification.notification_name}}" + name: "{{item.webhook_event_notification.name}}" description: "{{item.webhook_event_notification.description}}" - site_name_list: "{{item.webhook_event_notification.site_name_list}}" - events_name_list: "{{item.webhook_event_notification.events_name_list}}" - webhook_dest_name: "{{item.webhook_event_notification.webhook_dest_name}}" + site_names: "{{item.webhook_event_notification.site_names}}" + events_names: "{{item.webhook_event_notification.events_names}}" + destination_name: "{{item.webhook_event_notification.destination_name}}" email_event_notification: - notification_name: "{{item.email_event_notification.notification_name}}" + name: "{{item.email_event_notification.name}}" description: "{{item.email_event_notification.description}}" - site_name_list: "{{item.email_event_notification.site_name_list}}" - events_name_list: "{{item.email_event_notification.events_name_list}}" - from_email_address: "{{item.email_event_notification.from_email_address}}" - to_email_addresses: "{{item.email_event_notification.to_email_addresses}}" + site_names: "{{item.email_event_notification.site_names}}" + events_names: "{{item.email_event_notification.events_names}}" + sender_email: "{{item.email_event_notification.sender_email}}" + recipient_emails: "{{item.email_event_notification.recipient_emails}}" subject: "{{item.email_event_notification.subject}}" instance_name: "{{item.email_event_notification.instance_name}}" instance_description: "{{item.email_event_notification.instance_description}}" syslog_event_notification: - notification_name: "{{item.syslog_event_notification.notification_name}}" + name: "{{item.syslog_event_notification.name}}" description: "{{item.syslog_event_notification.description}}" - site_name_list: "{{item.syslog_event_notification.site_name_list}}" - events_name_list: "{{item.syslog_event_notification.events_name_list}}" - syslog_dest_name: "{{item.syslog_event_notification.syslog_dest_name}}" + site_names: "{{item.syslog_event_notification.site_names}}" + events_names: "{{item.syslog_event_notification.events_names}}" + destination_name: "{{item.syslog_event_notification.destination_name}}" with_items: "{{ events_notification }}" tags: diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index b378420177..7cf58e6075 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -257,6 +257,30 @@ def is_valid_password(self, password): return re.match(pattern, password) is not None + def is_valid_email(self, email): + """ + Validate an email address. + Args: + self (object): An instance of a class that provides access to Cisco Catalyst Center. + email (str): The email address to be validated. + Returns: + bool: True if the email is valid, False otherwise. + Description: + This function checks if the provided email address is valid based on the following criteria: + - It contains one or more alphanumeric characters or allowed special characters before the '@'. + - It contains one or more alphanumeric characters or dashes after the '@' and before the domain. + - It contains a period followed by at least two alphabetic characters at the end of the string. + The allowed special characters before the '@' are: ._%+-. + """ + + # Define the regex pattern for a valid email address + pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + # Use re.match to see if the email matches the pattern + if re.match(pattern, email): + return True + else: + return False + def get_dnac_params(self, params): """Store the Cisco Catalyst Center parameters from the playbook""" diff --git a/plugins/modules/events_and_notifications_workflow_manager.py b/plugins/modules/events_and_notifications_workflow_manager.py index f973654066..14c85e8617 100644 --- a/plugins/modules/events_and_notifications_workflow_manager.py +++ b/plugins/modules/events_and_notifications_workflow_manager.py @@ -301,7 +301,7 @@ type: dict suboptions: name: - description: Name of the Webhook event subscription notification . + description: Name of the Webhook event subscription notification. type: str required: True description: @@ -309,27 +309,26 @@ type: str required: True version: - description: Version of the event subscription notification . + description: Version label for the event subscription, helping track updates or changes. type: str - webhook_dest_name: - description: Name of the Webhook destination that is been configured to send the event subscription notification. And although - it's list but we have to give the name of only one Webhook destination. + destination_name: + description: The single Webhook destination's name for sending event notifications. type: str required: True - events_name_list: + events_names: description: List of the event names to be subscribed while configuring notification (For example, ["AP Flap", "AP Reboot Crash"]). type: list elements: str required: True - domain: - description: Name of the domain for the events (For example, Know Your Network, Connectivity etc.). + domain_name: + description: Broad category or domain under which the events fall (e.g., Know Your Network, Connectivity etc.). type: str - sub_domains: - description: List of subdomains within the specified domain (For example, ["Wireless", "Applications"]). + subdomain_names: + description: More specific categories within the main domain to further classify the events (e.g., ["Wireless", "Applications"]). type: list elements: str event_types: - description: List of event types to be included in the subscription of a notification (For example, ["APP", "NETWORK"]). + description: Types of events that trigger the notifications, defining the nature of the event (e.g., ["APP", "NETWORK"]). type: list elements: str event_categories: @@ -345,62 +344,62 @@ description: List of event sources to be included in the subscription of a notification. type: list elements: str - site_name_list: + site_names: description: List of site names where events are to be included in the subscription of a notification. (For example, ["Global/India", "Global/USA"]). type: list elements: str email_event_notification: - description: Dictionary containing the details for creating/updating the Email Event subscription notification in Cisco Catalyst - Center. Here you can create the email subscription notification as well as the create/update the email instance as well. + description: Configuration for setting up or modifying an Email Event Subscription in Cisco Catalyst Center. + This includes parameters for the email notification itself as well as details for the associated email instance. type: dict suboptions: name: - description: Name of the Webhook event subscription notification . + description: Name of the Email event subscription notification. type: str required: True description: - description: A brief explanation detailing the purpose of the email events subscription notification. + description: A brief explanation detailing the purpose of the Email events subscription notification. type: str required: True version: - description: Version of the event subscription notification . + description: Version label for the event subscription, helping track updates or changes. type: str - events_name_list: + events_names: description: List of the event names to be subscribed while configuring notification (For example, ["AP Flap", "AP Reboot Crash"]). type: list elements: str required: True - from_email_address: - description: The email address from which the notification is sent. + sender_email: + description: Originating email address for sending out the notifications. type: str required: True - to_email_addresses: - description: A list of email addresses to which the notification is sent. + recipient_emails: + description: Recipient email addresses that will receive the notifications. type: list elements: str required: True subject: - description: The subject line of the notification email. + description: The Subject line for the email notification, briefly indicating the notification content. type: str required: True instance_name: - description: The name of the email instance related to the notification. + description: Name assigned to the specific email instance used for sending the notification. type: str required: True instance_description: - description: A brief description of the email instance releated to the notification. + description: Detailed explanation of the email instance's purpose and how it relates to the notifications. type: str required: True - domain: - description: Name of the domain for the events (For example, Know Your Network, Connectivity etc.). + domain_name: + description: Broad category or domain under which the events fall (e.g., Know Your Network, Connectivity etc.). type: str - sub_domains: - description: List of subdomains within the specified domain (For example, ["Wireless", "Applications"]). + subdomain_names: + description: More specific categories within the main domain to further classify the events (e.g., ["Wireless", "Applications"]). type: list elements: str event_types: - description: List of event types to be included in the subscription of a notification (For example, ["APP", "NETWORK"]). + description: Types of events that trigger the notifications, defining the nature of the event (e.g., ["APP", "NETWORK"]). type: list elements: str event_categories: @@ -416,18 +415,18 @@ description: List of event sources to be included in the subscription of a notification. type: list elements: str - site_name_list: + site_names: description: List of site names where events are to be included in the subscription of a notification. (For example, ["Global/India", "Global/USA"]). type: list elements: str syslog_event_notification: - description: Dictionary containing the details for creating/updating the Syslog Event subscription notification in Cisco Catalyst - Center. + description: Configuration for establishing or revising a Syslog Event Subscription in the Cisco Catalyst Center. + This allows for the specification of Syslog event notification parameters and destination settings. type: dict suboptions: name: - description: Name of the Syslog event subscription notification . + description: Name of the Syslog event subscription notification. type: str required: True description: @@ -435,27 +434,26 @@ type: str required: True version: - description: Version of the event subscription notification . + description: Version label for the event subscription, helping track updates or changes. type: str - syslog_dest_name: - description: Name of the Syslog destination that is been configured to send the event subscription notification. And although - it's list but we have to give the name of only one Syslog destination. + destination_name: + description: The single Syslog destination's name for sending event notifications. type: str required: True - events_name_list: + events_names: description: List of the event names to be subscribed while configuring notification (For example, ["AP Flap", "AP Reboot Crash"]). type: list elements: str required: True - domain: - description: Name of the domain for the events (For example, Know Your Network, Connectivity etc.). + domain_name: + description: Broad category or domain under which the events fall (e.g., Know Your Network, Connectivity etc.). type: str - sub_domains: - description: List of subdomains within the specified domain (For example, ["Wireless", "Applications"]). + subdomain_names: + description: More specific categories within the main domain to further classify the events (e.g., ["Wireless", "Applications"]). type: list elements: str event_types: - description: List of event types to be included in the subscription of a notification (For example, ["APP", "NETWORK"]). + description: Types of events that trigger the notifications, defining the nature of the event (e.g., ["APP", "NETWORK"]). type: list elements: str event_categories: @@ -471,7 +469,7 @@ description: List of event sources to be included in the subscription of a notification. type: list elements: str - site_name_list: + site_names: description: List of site names where events are to be included in the subscription of a notification. (For example, ["Global/India", "Global/USA"]). type: list @@ -503,7 +501,22 @@ events.Events.get_itsm_integration_setting_by_id, events.Events.create_itsm_integration_setting, events.Events.update_itsm_integration_setting, - events.Events.delete_itsm_integration_setting + events.Events.delete_itsm_integration_setting, + events.Events.get_eventartifacts, + events.Events.get_site, + events.Events.get_syslog_event_subscriptions, + events.Events.get_syslog_subscription_details, + events.Events.create_syslog_event_subscription, + events.Events.update_syslog_event_subscription, + events.Events.get_rest_webhook_event_subscriptions, + events.Events.get_rest_webhook_subscription_details, + events.Events.create_rest_webhook_event_subscription, + events.Events.update_rest_webhook_event_subscription, + events.Events.get_email_event_subscriptions, + events.Events.get_email_subscription_details, + events.Events.create_email_event_subscription, + events.Events.update_email_event_subscription, + events.Events.delete_event_subscriptions """ @@ -726,9 +739,9 @@ - webhook_event_notification: name: "Webhook Notification." description: "Notification for webhook events subscription" - site_name_list: ["Global/India", "Global/USA"] - events_name_list: ["AP Flap", "AP Reboot Crash"] - webhook_dest_name: "Webhook Demo" + site_names: ["Global/India", "Global/USA"] + events_names: ["AP Flap", "AP Reboot Crash"] + destination_name: "Webhook Demo" - name: Updating Webhook Notification with the list of names of subscribed events in the system. cisco.dnac.events_and_notifications_workflow_manager: @@ -746,8 +759,8 @@ - webhook_event_notification: name: "Webhook Notification." description: "Updated notification for webhook events subscription" - site_name_list: ["Global/India", "Global/USA", "Global/China"] - webhook_dest_name: "Webhook Demo" + site_names: ["Global/India", "Global/USA", "Global/China"] + destination_name: "Webhook Demo" - name: Creating Email Notification with the list of names of subscribed events in the system. cisco.dnac.events_and_notifications_workflow_manager: @@ -765,10 +778,10 @@ - email_event_notification: name: "Email Notification" description: "Notification description for email subscription creation" - site_name_list: ["Global/India", "Global/USA"] - events_name_list: ["AP Flap", "AP Reboot Crash"] - from_email_address: "catalyst@cisco.com" - to_email_addresses: ["test@cisco.com", "demo@cisco.com"] + site_names: ["Global/India", "Global/USA"] + events_names: ["AP Flap", "AP Reboot Crash"] + sender_email: "catalyst@cisco.com" + recipient_emails: ["test@cisco.com", "demo@cisco.com"] subject: "Mail test" instance_name: Email Instance test @@ -788,10 +801,10 @@ - email_event_notification: name: "Email Notification" description: "Notification description for email subscription updation" - site_name_list: ["Global/India", "Global/USA"] - events_name_list: ["AP Flap", "AP Reboot Crash"] - from_email_address: "catalyst@cisco.com" - to_email_addresses: ["test@cisco.com", "demo@cisco.com", "update@cisco.com"] + site_names: ["Global/India", "Global/USA"] + events_names: ["AP Flap", "AP Reboot Crash"] + sender_email: "catalyst@cisco.com" + recipient_emails: ["test@cisco.com", "demo@cisco.com", "update@cisco.com"] subject: "Mail test for updation" instance_name: Email Instance test @@ -811,9 +824,9 @@ - syslog_event_notification: name: "Syslog Notification." description: "Notification for syslog events subscription" - site_name_list: ["Global/India", "Global/USA"] - events_name_list: ["AP Flap", "AP Reboot Crash"] - syslog_dest_name: "Syslog Demo" + site_names: ["Global/India", "Global/USA"] + events_names: ["AP Flap", "AP Reboot Crash"] + destination_name: "Syslog Demo" - name: Updating Syslog Notification with the list of names of subscribed events in the system. cisco.dnac.events_and_notifications_workflow_manager: @@ -831,8 +844,8 @@ - syslog_event_notification: name: "Syslog Notification." description: "Updated notification for syslog events subscription" - site_name_list: ["Global/India", "Global/USA", "Global/China"] - events_name_list: ["AP Flap", "AP Reboot Crash"] + site_names: ["Global/India", "Global/USA", "Global/China"] + events_names: ["AP Flap", "AP Reboot Crash"] - name: Deleting ITSM Integration Setting with given name from the system. cisco.dnac.events_and_notifications_workflow_manager: @@ -1029,11 +1042,11 @@ def validate_input(self): 'name': {'type': 'str'}, 'version': {'type': 'str'}, 'description': {'type': 'str'}, - 'site_name_list': {'type': 'list', 'elements': 'str'}, - 'events_name_list': {'type': 'list', 'elements': 'str'}, - 'webhook_dest_name': {'type': 'str'}, - 'domain': {'type': 'str'}, - 'sub_domains': {'type': 'list', 'elements': 'str'}, + 'site_names': {'type': 'list', 'elements': 'str'}, + 'events_names': {'type': 'list', 'elements': 'str'}, + 'destination_name': {'type': 'str'}, + 'domain_name': {'type': 'str'}, + 'subdomain_names': {'type': 'list', 'elements': 'str'}, 'event_types': {'type': 'list', 'elements': 'str'}, 'event_categories': {'type': 'list', 'elements': 'str'}, 'event_severities': {'type': 'list', 'elements': 'str'}, @@ -1044,15 +1057,15 @@ def validate_input(self): 'name': {'type': 'str'}, 'version': {'type': 'str'}, 'description': {'type': 'str'}, - 'site_name_list': {'type': 'list', 'elements': 'str'}, - 'events_name_list': {'type': 'list', 'elements': 'str'}, - 'from_email_address': {'type': 'str'}, - 'to_email_addresses': {'type': 'list', 'elements': 'str'}, + 'site_names': {'type': 'list', 'elements': 'str'}, + 'events_names': {'type': 'list', 'elements': 'str'}, + 'sender_email': {'type': 'str'}, + 'recipient_emails': {'type': 'list', 'elements': 'str'}, 'subject': {'type': 'str'}, 'instance_name': {'type': 'str'}, 'instance_description': {'type': 'str'}, - 'domain': {'type': 'str'}, - 'sub_domains': {'type': 'list', 'elements': 'str'}, + 'domain_name': {'type': 'str'}, + 'subdomain_names': {'type': 'list', 'elements': 'str'}, 'event_types': {'type': 'list', 'elements': 'str'}, 'event_categories': {'type': 'list', 'elements': 'str'}, 'event_severities': {'type': 'list', 'elements': 'str'}, @@ -1063,11 +1076,11 @@ def validate_input(self): 'name': {'type': 'str'}, 'version': {'type': 'str'}, 'description': {'type': 'str'}, - 'site_name_list': {'type': 'list', 'elements': 'str'}, - 'events_name_list': {'type': 'list', 'elements': 'str'}, - 'syslog_dest_name': {'type': 'str'}, - 'domain': {'type': 'str'}, - 'sub_domains': {'type': 'list', 'elements': 'str'}, + 'site_names': {'type': 'list', 'elements': 'str'}, + 'events_names': {'type': 'list', 'elements': 'str'}, + 'destination_name': {'type': 'str'}, + 'domain_name': {'type': 'str'}, + 'subdomain_names': {'type': 'list', 'elements': 'str'}, 'event_types': {'type': 'list', 'elements': 'str'}, 'event_categories': {'type': 'list', 'elements': 'str'}, 'event_severities': {'type': 'list', 'elements': 'str'}, @@ -1346,7 +1359,7 @@ def add_syslog_destination(self, syslog_details): except Exception as e: self.status = "failed" - self.msg = "Error while Adding the Syslog destination with the name '{0}' in Cisco Catalyst Center: {1}".format(name, str(e)) + self.msg = "Error while adding the Syslog destination with the name '{0}' in Cisco Catalyst Center: {1}".format(name, str(e)) self.log(self.msg, "ERROR") return self @@ -1415,7 +1428,7 @@ def update_syslog_destination(self, syslog_details, syslog_details_in_ccc): except Exception as e: self.status = "failed" - self.msg = "Error while Updating the Syslog destination with the name '{0}' in Cisco Catalyst Center: {1}".format(name, str(e)) + self.msg = "Error while updating the Syslog destination with the name '{0}' in Cisco Catalyst Center: {1}".format(name, str(e)) self.log(self.msg, "ERROR") return self @@ -1620,7 +1633,7 @@ def add_snmp_destination(self, snmp_params): except Exception as e: self.status = "failed" - self.msg = """Error while Adding the SNMP destination with the name '{0}' in Cisco Catalyst Center: + self.msg = """Error while adding the SNMP destination with the name '{0}' in Cisco Catalyst Center: {1}""".format(snmp_params.get('name'), str(e)) self.log(self.msg, "ERROR") self.check_return_status() @@ -1740,7 +1753,7 @@ def update_snmp_destination(self, snmp_params, snmp_dest_detail_in_ccc): except Exception as e: self.status = "failed" - self.msg = "Error while Updating the SNMP destination with name '{0}' in Cisco Catalyst Center: {1}".format(update_snmp_params.get('name'), str(e)) + self.msg = "Error while updating the SNMP destination with name '{0}' in Cisco Catalyst Center: {1}".format(update_snmp_params.get('name'), str(e)) self.log(self.msg, "ERROR") return self @@ -1857,7 +1870,7 @@ def add_webhook_destination(self, webhook_params): except Exception as e: self.status = "failed" - self.msg = "Error while Adding the Webhook destination with the name '{0}' in Cisco Catalyst Center: {1}".format(webhook_params.get('name'), str(e)) + self.msg = "Error while adding the Webhook destination with the name '{0}' in Cisco Catalyst Center: {1}".format(webhook_params.get('name'), str(e)) self.log(self.msg, "ERROR") return self @@ -1959,7 +1972,7 @@ def update_webhook_destination(self, webhook_params, webhook_dest_detail_in_ccc) except Exception as e: self.status = "failed" - self.msg = "Error while Updating the Rest Webhook destination with the name '{0}' in Cisco Catalyst Center: {1}".format(name, str(e)) + self.msg = "Error while updating the Rest Webhook destination with the name '{0}' in Cisco Catalyst Center: {1}".format(name, str(e)) self.log(self.msg, "ERROR") return self @@ -2114,7 +2127,7 @@ def add_email_destination(self, email_params): except Exception as e: self.status = "failed" - self.msg = "Error while Adding the Email destination in Cisco Catalyst Center: {0}".format(str(e)) + self.msg = "Error while adding the Email destination in Cisco Catalyst Center: {0}".format(str(e)) self.log(self.msg, "ERROR") return self @@ -2215,7 +2228,7 @@ def update_email_destination(self, email_details, email_dest_detail_in_ccc): except Exception as e: self.status = "failed" - self.msg = "Error while Updating the Email destination in Cisco Catalyst Center: {0}".format(str(e)) + self.msg = "Error while updating the Email destination in Cisco Catalyst Center: {0}".format(str(e)) self.log(self.msg, "ERROR") return self @@ -2532,7 +2545,7 @@ def update_itsm_integration_setting(self, itsm_params, itsm_in_ccc): except Exception as e: self.status = "failed" - self.msg = """Error while Updating the ITSM Integration Settings with name '{0}' in Cisco Catalyst Center due to: + self.msg = """Error while updating the ITSM Integration Settings with name '{0}' in Cisco Catalyst Center due to: {1}""".format(update_itsm_params.get('name'), str(e)) self.log(self.msg, "ERROR") @@ -2612,23 +2625,25 @@ def get_syslog_notification_details(self): except Exception as e: self.status = "failed" - self.msg = """Error while getting the details of Syslog Event subscription Notification present in - Cisco Catalyst Center: {0}""".format(str(e)) + self.msg = ( + "An error occurred while retrieving Syslog Event subscription Notification details " + "from Cisco Catalyst Center: {0}".format(repr(e)) + ) self.log(self.msg, "ERROR") self.check_return_status() - def get_syslog_subscription_detail(self, syslog_dest_name): + def get_syslog_subscription_detail(self, destination_name): """ Retrieves the details of a specific Syslog destination subscription from the Cisco Catalyst Center. Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - syslog_dest_name (str): The name of the Syslog destination for which details needs to be fetched. + destination_name (str): The name of the Syslog destination for which details needs to be fetched. Returns: dict or list: A dictionary containing the details of the Syslog destination subscription if found. Returns an empty list if no destination is found or if an error occurs during the API call. Description: This function calls an API to fetch the details of all Syslog destination from the Cisco Catalyst Center. - It then searches for a subscription that matches the given `syslog_dest_name`. If a match is found, it returns + It then searches for a subscription that matches the given `destination_name`. If a match is found, it returns details of the matching subscription. If no match is found or if an error occurs, it logs the appropriate message and handles the exception accordingly. """ @@ -2646,25 +2661,27 @@ def get_syslog_subscription_detail(self, syslog_dest_name): return sys_destination_details for dest in response: - if dest["name"] == syslog_dest_name: + if dest["name"] == destination_name: return dest - self.log("There is no Syslog destination with given name '{0}' present in Cisco Catalyst Center.".format(syslog_dest_name), "INFO") + self.log("Syslog destination with the name '{0}' not found in Cisco Catalyst Center.".format(destination_name), "INFO") return sys_destination_details except Exception as e: self.status = "failed" - self.msg = """Error while getting the details of Syslog Subscription with given name '{0}' present in - Cisco Catalyst Center: {1}""".format(syslog_dest_name, str(e)) + self.msg = ( + "Error while getting the details of the Syslog Subscription with the given name '{0}'" + " from Cisco Catalyst Center: {1}".format(destination_name, repr(e)) + ) self.log(self.msg, "ERROR") self.check_return_status() - def get_event_ids(self, events_name_list): + def get_event_ids(self, events_names): """ Retrieves the event IDs for a given list of event names from the Cisco Catalyst Center. Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - events_name_list (list of str): A list of event names for which the event IDs need to be retrieved. + events_names (list of str): A list of event names for which the event IDs need to be retrieved. Returns: list of str: A list of event IDs corresponding to the provided event names. If an event name is not found, it is skipped. @@ -2677,7 +2694,7 @@ def get_event_ids(self, events_name_list): event_ids = [] - for event_name in events_name_list: + for event_name in events_names: try: response = self.dnac._exec( family="event_management", @@ -2704,12 +2721,12 @@ def get_event_ids(self, events_name_list): return event_ids - def get_site_ids(self, site_name_list): + def get_site_ids(self, site_names): """ Retrieves the site IDs for a given list of site names from the Cisco Catalyst Center. Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - site_name_list (list of str): A list of site names for which the site IDs need to be retrieved. + site_names (list of str): A list of site names for which the site IDs need to be retrieved. Returns: list of str: A list of site IDs corresponding to the provided site names. If a site name is not found, it is skipped and return empty list. @@ -2721,25 +2738,28 @@ def get_site_ids(self, site_name_list): """ site_ids = [] - for site_name in site_name_list: + for site in site_names: try: response = self.dnac._exec( family="sites", function='get_site', op_modifies=True, - params={"name": site_name}, + params={"name": site}, ) self.log("Received API response from 'get_site': {0}".format(str(response)), "DEBUG") response = response.get('response') if not response: - self.log("There is no Site with name '{0}' present in Cisco Catalyst Center.".format(site_name), "INFO") + self.log("No site with the name '{0}' found in Cisco Catalyst Center.".format(site), "INFO") continue site_id = response[0].get("id") + if not site_id: + self.log("Site '{0}' found, but no ID available in the response.".format(site), "WARNING") + continue site_ids.append(site_id) except Exception as e: self.msg = """Error while getting the details of Site with given name '{0}' present in - Cisco Catalyst Center: {1}""".format(site_name, str(e)) + Cisco Catalyst Center: {1}""".format(site, str(e)) self.log(self.msg, "ERROR") return site_ids @@ -2771,15 +2791,16 @@ def collect_syslog_notification_playbook_params(self, syslog_notification_detail } # Collect the Instance ID of the syslog destination - syslog_dest_name = syslog_notification_details.get('syslog_dest_name') + self.log("Collecting parameters for Syslog Event Notification named '{0}'.".format(name), "INFO") + destination_name = syslog_notification_details.get('destination_name') - if syslog_dest_name: - subscription_details = self.get_syslog_subscription_detail(syslog_dest_name) + if destination_name: + subscription_details = self.get_syslog_subscription_detail(destination_name) if not subscription_details: self.status = "failed" self.msg = """Unable to create/update the syslog event notification '{0}' as syslog desination '{1}' is not configured or - present in Cisco Catalyst Center""".format(name, syslog_dest_name) + present in Cisco Catalyst Center""".format(name, destination_name) self.log(self.msg, "ERROR") self.check_return_status() @@ -2793,54 +2814,54 @@ def collect_syslog_notification_playbook_params(self, syslog_notification_detail } playbook_params["subscriptionEndpoints"].append(temp_subscript_endpoint) - events_name_list = syslog_notification_details.get('events_name_list') - if events_name_list: - events_ids = self.get_event_ids(events_name_list) - + events_names = syslog_notification_details.get('events_names') + if events_names: + events_ids = self.get_event_ids(events_names) if not events_ids: self.status = "failed" - self.msg = "Unable to create/update Syslog event notification as given events name '{0}' are incorrect.".format(str(events_name_list)) + self.msg = ( + "Unable to create/update Syslog event notification as the given event names '{0}' " + "are incorrect or could not be found." + ).format(str(events_names)) self.log(self.msg, "ERROR") self.check_return_status() playbook_params["filter"]["eventIds"] = events_ids - domain = syslog_notification_details.get("domain") - sub_domains = syslog_notification_details.get("sub_domains") - if domain and sub_domains: + domain_name = syslog_notification_details.get("domain_name") + subdomain_names = syslog_notification_details.get("subdomain_names") + if domain_name and subdomain_names: playbook_params["filter"]["domainsSubdomains"] = [] domain_dict = { - "domain": domain, - "subDomains": sub_domains + "domain": domain_name, + "subDomains": subdomain_names } playbook_params["filter"]["domainsSubdomains"].append(domain_dict) - event_types = syslog_notification_details.get("event_types") - if event_types: - playbook_params["filter"]["types"] = event_types - - event_categories = syslog_notification_details.get("event_categories") - if event_categories: - playbook_params["filter"]["categories"] = event_categories - - event_severities = syslog_notification_details.get("event_severities") - if event_severities: - playbook_params["filter"]["severities"] = event_severities - - event_sources = syslog_notification_details.get("event_sources") - if event_sources: - playbook_params["filter"]["sources"] = event_sources + # Add other filter parameters if present + filter_keys = ["event_types", "event_categories", "event_severities", "event_sources"] + filter_mapping = { + "event_types": "types", + "event_categories": "categories", + "event_severities": "severities", + "event_sources": "sources" + } - site_name_list = syslog_notification_details.get("site_name_list") - if site_name_list: - site_ids = self.get_site_ids(site_name_list) + for key in filter_keys: + value = syslog_notification_details.get(key) + if value: + playbook_params["filter"][filter_mapping[key]] = value + site_names = syslog_notification_details.get("site_names") + if site_names: + site_ids = self.get_site_ids(site_names) if not site_ids: - self.msg = "Unable to find the Site ID's for the given site(s) - '{0}' in the playbook's input.".format(str(site_name_list)) + self.msg = "Unable to find the Site ID's for the given site(s) - '{0}' in the playbook's input.".format(site_names) self.log(self.msg, "INFO") playbook_params["filter"]["siteIds"] = site_ids syslog_notification_params.append(playbook_params) + self.log("Syslog notification playbook parameters collected successfully for '{0}': {1}".format(name, playbook_params), "INFO") return syslog_notification_params @@ -2872,12 +2893,12 @@ def mandatory_syslog_notification_parameter_check(self, syslog_notification_para subs_endpoints = syslog_notification_params.get('subscriptionEndpoints') if not subs_endpoints: - required_params_absent.append("syslog_dest_name") + required_params_absent.append("destination_name") filters = syslog_notification_params.get("filter") if not filters.get("eventIds"): - required_params_absent.append("events_name_list") + required_params_absent.append("events_names") if required_params_absent: self.status = "failed" @@ -2943,7 +2964,7 @@ def create_syslog_notification(self, syslog_notification_params): except Exception as e: self.status = "failed" - self.msg = """Error while Adding the Syslog Event Subscription Notification with name '{0}' in Cisco Catalyst Center: + self.msg = """Error while adding the Syslog Event Subscription Notification with name '{0}' in Cisco Catalyst Center: {1}""".format(notification_name, str(e)) self.log(self.msg, "ERROR") @@ -2989,31 +3010,36 @@ def compare_notification_filters(self, filters_in_playbook, filters_in_ccc): for key, value in filters_in_playbook.items(): if key == "domainsSubdomains": - domain_input = filters_in_playbook.get("domainsSubdomains")[0].get("domain") - subdomains_input = filters_in_playbook.get("domainsSubdomains")[0].get("subDomains") + domain_subdomain_input = filters_in_playbook.get("domainsSubdomains") domain_subdomain_in_ccc = filters_in_ccc.get("domainsSubdomains") + if domain_subdomain_input: # Ensure that there is input for 'domainsSubdomains' + domain_input = domain_subdomain_input[0].get("domain") + subdomains_input = domain_subdomain_input[0].get("subDomains") + else: + domain_input = subdomains_input = None + if not domain_subdomain_in_ccc: - self.log("Since no domain subDomains present in Catalyst Center so notification needs update.", "INFO") + self.log("Since no domain or subdomains are present in Catalyst Center, the notification needs an update.", "INFO") return True domain_in_ccc = domain_subdomain_in_ccc.get("domain") subdomain_in_ccc = domain_subdomain_in_ccc.get("subDomains") if domain_input and domain_input != domain_in_ccc: - self.log("Domain '{0}' given in the playbook doesnot match with domain in Cisco Catalyst Center".format(domain_input), "INFO") + self.log("Domain '{0}' given in the playbook does not match with domain in Cisco Catalyst Center".format(domain_input), "INFO") return True if subdomains_input: list_needs_update = self.is_element_missing(subdomains_input, subdomain_in_ccc) if list_needs_update: - self.log("""Given sub_domains '{0}' in the playbook doesnot match with the value present in Cisco Catalyst Center - so notification needs update.""".format(subdomains_input), "INFO") + self.log(("Given subdomain_names '{0}' in the playbook do not match with the values present in " + "Cisco Catalyst Center, so the notification needs an update.").format(subdomains_input), "INFO") return True elif isinstance(value, list): list_needs_update = self.is_element_missing(value, filters_in_ccc[key]) if list_needs_update: - self.log("""Parameter '{0}' given in the playbook doesnot match with the value present in Cisco Catalyst Center - so notification needs update.""".format(key), "INFO") + self.log(("Parameter '{0}' given in the playbook does not match with the value present in Cisco Catalyst " + "Center so notification needs update.").format(key), "INFO") return True return False @@ -3043,23 +3069,27 @@ def syslog_notification_needs_update(self, syslog_notification_params, syslog_no ccc_endpoints = syslog_notification_in_ccc.get("subscriptionEndpoints")[0] if description_in_playbook and description_in_playbook != description_in_ccc: - self.log("""Parameter description doesnot match with the value of description present in Cisco Catalyst Center - so given Syslog Event Notification {0} needs an update""".format(name), "INFO") + self.log("Parameter 'description' does not match with the value of description present in Cisco Catalyst Center " + "so given Syslog Event Notification '{0}' needs an update".format(name), "INFO") return True if subs_endpoints: instance_id = subs_endpoints[0].get("instanceId") ccc_instance_id = ccc_endpoints.get("instanceId") if instance_id != ccc_instance_id: - self.log("""Given Syslog destination in the playbook is different from syslog destination present in Cisco Catalyst Center - so given Syslog Event Notification {0} needs an update""".format(name), "INFO") + self.log("Given Syslog destination in the playbook is different from Syslog destination present in Cisco Catalyst Center " + "so given Syslog Event Notification '{0}' needs an update".format(name), "INFO") return True + filters_in_playbook = syslog_notification_params.get("filter") filters_in_ccc = syslog_notification_in_ccc.get("filter") - filters_need_update = self.compare_notification_filters(filters_in_playbook, filters_in_ccc) + if self.compare_notification_filters(filters_in_playbook, filters_in_ccc): + self.log("Notification filters differ between the playbook and Cisco Catalyst Center. Syslog Event Subscription Notification " + "'{0}' needs an update.".format(name), "INFO") + return True - return filters_need_update + return False def collect_notification_filter_params(self, playbook_params, filter, ccc_filter): """ @@ -3078,23 +3108,15 @@ def collect_notification_filter_params(self, playbook_params, filter, ccc_filter with the filter parameters from Cisco Catalyst Center. """ + filter_keys = ["eventIds", "domainsSubdomains", "types", "categories", "severities", "sources", "siteIds"] + if filter: - playbook_params["filter"]["eventIds"] = filter.get("eventIds") or ccc_filter.get("eventIds") - playbook_params["filter"]["domainsSubdomains"] = filter.get("domainsSubdomains") or ccc_filter.get("domainsSubdomains") - playbook_params["filter"]["types"] = filter.get("types") or ccc_filter.get("types") - playbook_params["filter"]["categories"] = filter.get("categories") or ccc_filter.get("categories") - playbook_params["filter"]["severities"] = filter.get("severities") or ccc_filter.get("severities") - playbook_params["filter"]["sources"] = filter.get("sources") or ccc_filter.get("sources") - playbook_params["filter"]["siteIds"] = filter.get("siteIds") or ccc_filter.get("siteIds") + for key in filter_keys: + playbook_params["filter"][key] = filter.get(key) or ccc_filter.get(key) else: # Need to take all required/optional parameter from Cisco Catalyst Center - playbook_params["filter"]["eventIds"] = ccc_filter.get("eventIds") - playbook_params["filter"]["domainsSubdomains"] = ccc_filter.get("domainsSubdomains") - playbook_params["filter"]["types"] = ccc_filter.get("types") - playbook_params["filter"]["categories"] = ccc_filter.get("categories") - playbook_params["filter"]["severities"] = ccc_filter.get("severities") - playbook_params["filter"]["sources"] = ccc_filter.get("sources") - playbook_params["filter"]["siteIds"] = ccc_filter.get("siteIds") + for key in filter_keys: + playbook_params["filter"][key] = ccc_filter.get(key) return playbook_params @@ -3103,8 +3125,8 @@ def update_syslog_notification(self, syslog_notification_params, syslog_notifica Updates a Syslog Event Notification subscription in Cisco Catalyst Center. Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - syslog_notification_params (dict): Dictionary containing parameters for updating the Syslog Event Notification. - syslog_notification_in_ccc (dict): Dictionary containing current configuration of the Syslog Event Notification in CCC. + syslog_notification_params (dict): Parameters for updating the Syslog Event Notification. + syslog_notification_in_ccc (dict): Current configuration of the Syslog Event Notification in CCC. Returns: self (object): An instance of a class representing the status of the operation, including whether it was @@ -3118,15 +3140,12 @@ def update_syslog_notification(self, syslog_notification_params, syslog_notifica syslog_notification_params = syslog_notification_params[0] sys_notification_update_params = [] name = syslog_notification_params.get("name") - description = syslog_notification_params.get("description") or syslog_notification_in_ccc.get("description") - version = syslog_notification_params.get("version") or syslog_notification_in_ccc.get("version") - subscription_id = syslog_notification_in_ccc.get("subscriptionId") playbook_params = { - "subscriptionId": subscription_id, + "subscriptionId": syslog_notification_in_ccc.get("subscriptionId"), "name": name, - "description": description, - "version": version, + "description": syslog_notification_params.get("description") or syslog_notification_in_ccc.get("description"), + "version": syslog_notification_params.get("version") or syslog_notification_in_ccc.get("version"), "filter": {} } subs_endpoints = syslog_notification_params.get("subscriptionEndpoints") @@ -3136,13 +3155,12 @@ def update_syslog_notification(self, syslog_notification_params, syslog_notifica else: playbook_params["subscriptionEndpoints"] = [] instance_id = syslog_notification_in_ccc.get("subscriptionEndpoints")[0].get("instanceId") - temp_subscript_endpoint = { + playbook_params["subscriptionEndpoints"] = [{ "instanceId": instance_id, "subscriptionDetails": { "connectorType": "SYSLOG" } - } - playbook_params['subscriptionEndpoints'].append(temp_subscript_endpoint) + }] filter = syslog_notification_params.get("filter") ccc_filter = syslog_notification_in_ccc.get("filter") @@ -3218,23 +3236,22 @@ def get_webhook_notification_details(self): except Exception as e: self.status = "failed" - self.msg = """Error while getting the details of Webhook Event subscription notification present in - Cisco Catalyst Center: {0}""".format(str(e)) + self.log("Error while retrieving Webhook Event Notification details: {0}".format(str(e)), "ERROR") self.log(self.msg, "ERROR") self.check_return_status() - def get_webhook_subscription_detail(self, webhook_dest_name): + def get_webhook_subscription_detail(self, destination_name): """ Retrieves the details of a specific webhook destination subscription from the Cisco Catalyst Center. Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - webhook_dest_name (str): The name of the webhook destination for which details needs to be fetched. + destination_name (str): The name of the webhook destination for which details needs to be fetched. Returns: dict or list: A dictionary containing the details of the webhook destination subscription if found. Returns an empty list if no destination is found or if an error occurs during the API call. Description: This function calls an API to fetch the details of all webhook destination from the Cisco Catalyst Center. - It then searches for a subscription that matches the given `webhook_dest_name`. If a match is found, it returns + It then searches for a subscription that matches the given `destination_name`. If a match is found, it returns details of the matching subscription. If no match is found or if an error occurs, it logs the appropriate message and handles the exception accordingly. """ @@ -3252,16 +3269,16 @@ def get_webhook_subscription_detail(self, webhook_dest_name): return web_destination_details for dest in response: - if dest["name"] == webhook_dest_name: + if dest["name"] == destination_name: return dest - self.log("There is no webhook destination with given name '{0}' present in Cisco Catalyst Center.".format(webhook_dest_name), "INFO") + self.log("There is no webhook destination with given name '{0}' present in Cisco Catalyst Center.".format(destination_name), "INFO") return web_destination_details except Exception as e: self.status = "failed" self.msg = """Error while getting the details of webhook Subscription with given name '{0}' present in - Cisco Catalyst Center: {1}""".format(webhook_dest_name, str(e)) + Cisco Catalyst Center: {1}""".format(destination_name, str(e)) self.log(self.msg, "ERROR") self.check_return_status() @@ -3292,15 +3309,16 @@ def collect_webhook_notification_playbook_params(self, webhook_notification_deta } # Collect the Instance ID of the webhook destination - webhook_dest_name = webhook_notification_details.get('webhook_dest_name') + self.log("Collecting parameters for Webhook Event Notification named '{0}'.".format(name), "INFO") + destination_name = webhook_notification_details.get('destination_name') - if webhook_dest_name: - subscription_details = self.get_webhook_subscription_detail(webhook_dest_name) + if destination_name: + subscription_details = self.get_webhook_subscription_detail(destination_name) if not subscription_details: self.status = "failed" self.msg = """Unable to create/update the webhook event notification '{0}' as webhook desination '{1}' is not configured or - present in Cisco Catalyst Center""".format(name, webhook_dest_name) + present in Cisco Catalyst Center""".format(name, destination_name) self.log(self.msg, "ERROR") self.check_return_status() @@ -3314,53 +3332,55 @@ def collect_webhook_notification_playbook_params(self, webhook_notification_deta } playbook_params["subscriptionEndpoints"].append(temp_subscript_endpoint) - events_name_list = webhook_notification_details.get('events_name_list') - if events_name_list: - events_ids = self.get_event_ids(events_name_list) - + events_names = webhook_notification_details.get('events_names') + if events_names: + events_ids = self.get_event_ids(events_names) if not events_ids: self.status = "failed" - self.msg = "Unable to create/update webhook event notification as given events name '{0}' are incorrect.".format(str(events_name_list)) + self.msg = ( + "Unable to create/update Webhook event notification as the given event names '{0}' " + "are incorrect or could not be found." + ).format(str(events_names)) self.log(self.msg, "ERROR") self.check_return_status() playbook_params["filter"]["eventIds"] = events_ids - domain = webhook_notification_details.get("domain") - sub_domains = webhook_notification_details.get("sub_domains") - if domain and sub_domains: + domain_name = webhook_notification_details.get("domain_name") + subdomain_names = webhook_notification_details.get("subdomain_names") + if domain_name and subdomain_names: playbook_params["filter"]["domainsSubdomains"] = [] domain_dict = { - "domain": domain, - "subDomains": sub_domains + "domain": domain_name, + "subDomains": subdomain_names } playbook_params["filter"]["domainsSubdomains"].append(domain_dict) - event_types = webhook_notification_details.get("event_types") - if event_types: - playbook_params["filter"]["types"] = event_types - - event_categories = webhook_notification_details.get("event_categories") - if event_categories: - playbook_params["filter"]["categories"] = event_categories - - event_severities = webhook_notification_details.get("event_severities") - if event_severities: - playbook_params["filter"]["severities"] = event_severities - - event_sources = webhook_notification_details.get("event_sources") - if event_sources: - playbook_params["filter"]["sources"] = event_sources + # Add other filter parameters if present + filter_keys = ["event_types", "event_categories", "event_severities", "event_sources"] + filter_mapping = { + "event_types": "types", + "event_categories": "categories", + "event_severities": "severities", + "event_sources": "sources" + } - site_name_list = webhook_notification_details.get("site_name_list") - if site_name_list: - site_ids = self.get_site_ids(site_name_list) + for key in filter_keys: + value = webhook_notification_details.get(key) + if value: + playbook_params["filter"][filter_mapping[key]] = value + site_names = webhook_notification_details.get("site_names") + if site_names: + site_ids = self.get_site_ids(site_names) if not site_ids: - self.msg = "Unable to find the Site ID's for the given site(s) - '{0}' in the playbook's input.".format(str(site_name_list)) + self.msg = "Unable to find the Site IDs for the given site(s) - '{0}' in the playbook's input.".format(site_names) self.log(self.msg, "INFO") playbook_params["filter"]["siteIds"] = site_ids + self.log("Site IDs '{0}' found for site names '{1}'. Added to filter.".format(site_ids, site_names), "INFO") + + self.log("Webhook notification playbook parameters collected successfully for '{0}': {1}".format(name, playbook_params), "INFO") webhook_notification_params.append(playbook_params) return webhook_notification_params @@ -3380,9 +3400,11 @@ def mandatory_webhook_notification_parameter_check(self, webhook_notification_pa """ required_params_absent = [] - webhook_notification_params = webhook_notification_params[0] - notification_name = webhook_notification_params.get("name") - description = webhook_notification_params.get("description") + webhook_params = webhook_notification_params[0] + notification_name = webhook_params.get("name") + description = webhook_params.get("description") + subs_endpoints = webhook_params.get('subscriptionEndpoints') + filters = webhook_params.get("filter") if not notification_name: required_params_absent.append("name") @@ -3390,20 +3412,18 @@ def mandatory_webhook_notification_parameter_check(self, webhook_notification_pa if not description: required_params_absent.append("description") - subs_endpoints = webhook_notification_params.get('subscriptionEndpoints') - if not subs_endpoints: - required_params_absent.append("webhook_dest_name") - - filters = webhook_notification_params.get("filter") + required_params_absent.append("destination_name") if not filters.get("eventIds"): - required_params_absent.append("events_name_list") + required_params_absent.append("events_names") if required_params_absent: self.status = "failed" - self.msg = """Missing required parameter '{0}' for adding webhook Event Notification with given - name {1}""".format(str(required_params_absent), notification_name) + self.msg = ( + "Missing required parameter(s) '{0}' for adding Webhook Event Notification with the given " + "name '{1}'." + ).format(str(required_params_absent), notification_name) self.log(self.msg, "ERROR") self.check_return_status() @@ -3464,7 +3484,7 @@ def create_webhook_notification(self, webhook_notification_params): except Exception as e: self.status = "failed" - self.msg = "Error while Adding the webhook Event Notification with name '{0}' in Cisco Catalyst Center: {1}".format(notification_name, str(e)) + self.msg = "Error while adding the webhook Event Notification with name '{0}' in Cisco Catalyst Center: {1}".format(notification_name, str(e)) self.log(self.msg, "ERROR") return self @@ -3486,31 +3506,35 @@ def webhook_notification_needs_update(self, webhook_notification_params, webhook If all parameters match, it returns False indicating that no update is required. """ - webhook_notification_params = webhook_notification_params[0] - name = webhook_notification_params.get("name") - description_in_playbook = webhook_notification_params.get("description") + webhook_params = webhook_notification_params[0] + name = webhook_params.get("name") + description_in_playbook = webhook_params.get("description") description_in_ccc = webhook_notification_in_ccc.get("description") - subs_endpoints = webhook_notification_params.get("subscriptionEndpoints") + subs_endpoints = webhook_params.get("subscriptionEndpoints") ccc_endpoints = webhook_notification_in_ccc.get("subscriptionEndpoints")[0] if description_in_playbook and description_in_playbook != description_in_ccc: - self.log("""Parameter description doesnot match with the value of description present in Cisco Catalyst Center - so given webhook Event Subscription Notification {0} needs an update""".format(name), "INFO") + self.log("Parameter 'description' does not match with the value of description present in Cisco Catalyst Center " + "so given Webhook Event Notification '{0}' needs an update".format(name), "INFO") return True if subs_endpoints: instance_id = subs_endpoints[0].get("instanceId") ccc_instance_id = ccc_endpoints.get("instanceId") if instance_id != ccc_instance_id: - self.log("""Given webhook destination in the playbook is different from webhook destination present in Cisco Catalyst Center - so given webhook Event Subscription Notification {0} needs an update""".format(name), "INFO") + self.log("Given Webhook destination in the playbook is different from Webhook destination present in Cisco Catalyst " + "Center so given Webhook Event Subscription Notification '{0}' needs an update".format(name), "INFO") return True - filters_in_playbook = webhook_notification_params.get("filter") + + filters_in_playbook = webhook_params.get("filter") filters_in_ccc = webhook_notification_in_ccc.get("filter") - filters_need_update = self.compare_notification_filters(filters_in_playbook, filters_in_ccc) + if self.compare_notification_filters(filters_in_playbook, filters_in_ccc): + self.log("Notification filters differ between the playbook and Cisco Catalyst Center. Webhook Event Subscription Notification " + "'{0}' needs an update.".format(name), "INFO") + return True - return filters_need_update + return False def update_webhook_notification(self, webhook_notification_params, webhook_notification_in_ccc): """ @@ -3528,21 +3552,18 @@ def update_webhook_notification(self, webhook_notification_params, webhook_notif After the update operation, it checks the status of the API request and logs appropriate messages based on the response. """ - webhook_notification_params = webhook_notification_params[0] + webhook_params = webhook_notification_params[0] web_notification_update_params = [] - name = webhook_notification_params.get("name") - description = webhook_notification_params.get("description") or webhook_notification_in_ccc.get("description") - version = webhook_notification_params.get("version") or webhook_notification_in_ccc.get("version") - subscription_id = webhook_notification_in_ccc.get("subscriptionId") + name = webhook_params.get("name") playbook_params = { - "subscriptionId": subscription_id, + "subscriptionId": webhook_notification_in_ccc.get("subscriptionId"), "name": name, - "description": description, - "version": version, - "filter": {} + "description": webhook_params.get("description") or webhook_notification_in_ccc.get("description"), + "version": webhook_params.get("version") or webhook_notification_in_ccc.get("version"), + "filter": {}, } - subs_endpoints = webhook_notification_params.get("subscriptionEndpoints") + subs_endpoints = webhook_params.get("subscriptionEndpoints") if subs_endpoints: playbook_params["subscriptionEndpoints"] = subs_endpoints @@ -3557,7 +3578,7 @@ def update_webhook_notification(self, webhook_notification_params, webhook_notif } playbook_params['subscriptionEndpoints'].append(temp_subscript_endpoint) - filter = webhook_notification_params.get("filter") + filter = webhook_params.get("filter") ccc_filter = webhook_notification_in_ccc.get("filter") webhook_update_params = self.collect_notification_filter_params(playbook_params, filter, ccc_filter) web_notification_update_params.append(webhook_update_params) @@ -3597,8 +3618,7 @@ def update_webhook_notification(self, webhook_notification_params, webhook_notif except Exception as e: self.status = "failed" - self.msg = """Error while updating the Webhook Event Subscription Notification with name '{0}' in Cisco Catalyst - Center: {1}""".format(name, str(e)) + self.msg = "Exception occurred while updating Webhook Notification with name '{0}': {1}.".format(name, str(e)) self.log(self.msg, "ERROR") return self @@ -3631,8 +3651,7 @@ def get_email_notification_details(self): return response except Exception as e: self.status = "failed" - self.msg = """Error while getting the details of Email Event subscription notification present in - Cisco Catalyst Center: {0}""".format(str(e)) + self.msg = "Exception occurred while retrieving Email Event Subscription Notification: {0}".format(str(e)) self.log(self.msg, "ERROR") self.check_return_status() @@ -3705,32 +3724,43 @@ def collect_email_notification_playbook_params(self, email_notification_details) } # Collect the Instance ID of the email destination + self.log("Collecting parameters for Email Event Notification named '{0}'.".format(email_notf_name), "INFO") instance_name = email_notification_details.get('instance_name') if not instance_name: self.status = "failed" - self.msg = """Name parameter for the Subscription Endpoints is required for creating/updating Email Events Subscription - Notification with given name {0} in Cisco Catalyst Center""".format(email_notf_name) + self.msg = "Instance name for Subscription Endpoints is required for Email notification '{0}'.".format(email_notf_name) self.log(self.msg, "ERROR") return self subscription_details = self.get_email_subscription_detail(instance_name) instance_id = None - fromEmailAddress = email_notification_details.get("from_email_address") - toEmailAddresses = email_notification_details.get("to_email_addresses") - subject = email_notification_details.get("subject") - description = email_notification_details.get("instance_description") if subscription_details: instance_id = subscription_details.get("instanceId") - if not fromEmailAddress: - fromEmailAddress = subscription_details.get("fromEmailAddress") - if not toEmailAddresses: - toEmailAddresses = subscription_details.get("toEmailAddresses") - if not subject: - subject = subscription_details.get("subject") - if not description: - description = subscription_details.get("description") + fromEmailAddress = email_notification_details.get("sender_email") or subscription_details.get("fromEmailAddress") + toEmailAddresses = email_notification_details.get("recipient_emails") or subscription_details.get("toEmailAddresses") + subject = email_notification_details.get("subject") or subscription_details.get("subject") + description = email_notification_details.get("instance_description") or subscription_details.get("description") + + if not self.is_valid_email(fromEmailAddress): + self.status = "failed" + self.msg = ( + "Unable to create/update Email event notification as the given sender_email '{0}' " + "are incorrect or invalid given in the playbook." + ).format(fromEmailAddress) + self.log(self.msg, "ERROR") + self.check_return_status() + + for email in toEmailAddresses: + if not self.is_valid_email(email): + self.status = "failed" + self.msg = ( + "Unable to create/update Email event notification as the given recipient_email '{0}' " + "is incorrect or invalid given in the playbook." + ).format(email) + self.log(self.msg, "ERROR") + self.check_return_status() temp_subscript_endpoint = { "instanceId": instance_id, @@ -3744,55 +3774,61 @@ def collect_email_notification_playbook_params(self, email_notification_details) } } playbook_params["subscriptionEndpoints"].append(temp_subscript_endpoint) + else: + self.log("No subscription details found for instance '{0}'.".format(instance_name), "WARNING") - events_name_list = email_notification_details.get('events_name_list') - if events_name_list: - events_ids = self.get_event_ids(events_name_list) - + events_names = email_notification_details.get('events_names') + if events_names: + events_ids = self.get_event_ids(events_names) if not events_ids: self.status = "failed" - self.msg = "Unable to create/update email event notification as given events name '{0}' are incorrect.".format(str(events_name_list)) + self.msg = ( + "Unable to create/update Email event notification as the given event names '{0}' " + "are incorrect or could not be found." + ).format(str(events_names)) self.log(self.msg, "ERROR") self.check_return_status() playbook_params["filter"]["eventIds"] = events_ids - domain = email_notification_details.get("domain") - sub_domains = email_notification_details.get("sub_domains") - if domain and sub_domains: + domain_name = email_notification_details.get("domain_name") + subdomain_names = email_notification_details.get("subdomain_names") + if domain_name and subdomain_names: playbook_params["filter"]["domainsSubdomains"] = [] domain_dict = { - "domain": domain, - "subDomains": sub_domains + "domain": domain_name, + "subDomains": subdomain_names } playbook_params["filter"]["domainsSubdomains"].append(domain_dict) - event_types = email_notification_details.get("event_types") - if event_types: - playbook_params["filter"]["types"] = event_types - - event_categories = email_notification_details.get("event_categories") - if event_categories: - playbook_params["filter"]["categories"] = event_categories - - event_severities = email_notification_details.get("event_severities") - if event_severities: - playbook_params["filter"]["severities"] = event_severities - - event_sources = email_notification_details.get("event_sources") - if event_sources: - playbook_params["filter"]["sources"] = event_sources + # Add other filter parameters if present + filter_keys = ["event_types", "event_categories", "event_severities", "event_sources"] + filter_mapping = { + "event_types": "types", + "event_categories": "categories", + "event_severities": "severities", + "event_sources": "sources" + } - site_name_list = email_notification_details.get("site_name_list") - if site_name_list: - site_ids = self.get_site_ids(site_name_list) + for key in filter_keys: + value = email_notification_details.get(key) + if value: + playbook_params["filter"][filter_mapping[key]] = value + site_names = email_notification_details.get("site_names") + if site_names: + site_ids = self.get_site_ids(site_names) if not site_ids: - self.msg = "Unable to find the Site ID's for the given site(s) - '{0}' in the playbook's input.".format(str(site_name_list)) + self.msg = "Unable to find the Site IDs for the given site(s) - '{0}' in the playbook's input.".format(site_names) self.log(self.msg, "INFO") playbook_params["filter"]["siteIds"] = site_ids email_notification_params.append(playbook_params) + self.log( + "Email notification playbook parameters collected successfully for " + "'{0}': {1}" + .format(email_notf_name, playbook_params), "INFO" + ) return email_notification_params @@ -3824,29 +3860,29 @@ def mandatory_email_notification_parameter_check(self, email_notification_params subs_endpoints = email_notification_params.get('subscriptionEndpoints') if not subs_endpoints: - required_params_absent.extends(["instance_name", "fromEmailAddress", "to_email_addresses", "subject"]) + required_params_absent.extends(["instance_name", "fromEmailAddress", "toEmailAddresses", "subject"]) else: subs_endpoints = subs_endpoints[0].get("subscriptionDetails") if not subs_endpoints.get("fromEmailAddress"): - required_params_absent.append("from_email_address") + required_params_absent.append("sender_email") if not subs_endpoints.get("toEmailAddresses"): - required_params_absent.append("to_email_addresses") + required_params_absent.append("recipient_emails") if not subs_endpoints.get("subject"): required_params_absent.append("subject") if not subs_endpoints.get("name"): required_params_absent.append("instance_name") filters = email_notification_params.get("filter") - - if not filters.get("eventIds"): - required_params_absent.append("events_name_list") + if not filters: + required_params_absent.append("events_names") if required_params_absent: self.status = "failed" - self.msg = """Missing required parameter '{0}' for adding Email Events Subscription Notification - with given name {1}""".format(str(required_params_absent), notification_name) + missing_params = ", ".join(required_params_absent) + self.msg = "Missing required parameters [{0}] for adding Email Events Subscription Notification '{1}'.".format(missing_params, notification_name) self.log(self.msg, "ERROR") self.check_return_status() + self.log("All mandatory parameters for Email Event Subscription Notification are present.", "INFO") return self @@ -3905,7 +3941,7 @@ def create_email_notification(self, email_notification_params): except Exception as e: self.status = "failed" - self.msg = "Error while Adding the email Event Notification with name '{0}' in Cisco Catalyst Center: {1}".format(notification_name, str(e)) + self.msg = "Error while adding the Email Event Notification with name '{0}' in Cisco Catalyst Center: {1}".format(notification_name, str(e)) self.log(self.msg, "ERROR") return self @@ -3936,7 +3972,7 @@ def compare_email_subs_endpoints(self, subs_endpoints, ccc_endpoints): list_needs_update = self.is_element_missing(playbook_param, ccc_list_param) if list_needs_update: - self.log("""Parameter '{0}' given in the playbook doesnot match with the value present in Cisco Catalyst Center + self.log("""Parameter '{0}' given in the playbook does not match with the value present in Cisco Catalyst Center so notification needs update.""".format(param), "INFO") return True elif subs_endpoints.get(param) != ccc_endpoints.get(param): @@ -3969,24 +4005,28 @@ def email_notification_needs_update(self, email_notification_params, email_notif ccc_endpoints = email_notification_in_ccc.get("subscriptionEndpoints")[0] if description_in_playbook and description_in_playbook != description_in_ccc: - self.log("""Parameter description doesnot match with the value of description present in Cisco Catalyst Center - so given email Event Subscription Notification {0} needs an update""".format(name), "INFO") + self.log("Parameter 'description' does not match with the value of description present in Cisco Catalyst Center " + "so given Email Event Notification '{0}' needs an update".format(name), "INFO") return True if subs_endpoints: subs_endpoints = subs_endpoints[0] - notification_need_update = self.compare_email_subs_endpoints(subs_endpoints, ccc_endpoints) + notification_update = self.compare_email_subs_endpoints(subs_endpoints, ccc_endpoints) - if notification_need_update: - self.log("""Given Email destination in the playbook is different from email destination present in Cisco Catalyst Center - so given email Event Subscription Notification {0} needs an update""".format(name), "INFO") + if notification_update: + self.log("Given Email Instance details in the playbook is different from email instance present in Cisco Catalyst " + "Center so given email Event Subscription Notification {0} needs an update".format(name), "INFO") return True filters_in_playbook = email_notification_params.get("filter") filters_in_ccc = email_notification_in_ccc.get("filter") - filters_need_update = self.compare_notification_filters(filters_in_playbook, filters_in_ccc) - return filters_need_update + if self.compare_notification_filters(filters_in_playbook, filters_in_ccc): + self.log("Notification filters differ between the playbook and Cisco Catalyst Center. Email Event Subscription Notification " + "'{0}' needs an update.".format(name), "INFO") + return True + + return False def update_email_notification(self, email_notification_params, email_notification_in_ccc): """ @@ -4005,17 +4045,15 @@ def update_email_notification(self, email_notification_params, email_notificatio """ email_notification_params = email_notification_params[0] - email_notification_update_params = [] + notification_update_params = [] name = email_notification_params.get("name") - description = email_notification_params.get("description") or email_notification_in_ccc.get("description") - version = email_notification_params.get("version") or email_notification_in_ccc.get("version") - subscription_id = email_notification_in_ccc.get("subscriptionId") + # Prepare the parameters for the update operation playbook_params = { - "subscriptionId": subscription_id, + "subscriptionId": email_notification_in_ccc.get("subscriptionId"), "name": name, - "description": description, - "version": version, + "description": email_notification_params.get("description", email_notification_in_ccc.get("description")), + "version": email_notification_params.get("version", email_notification_in_ccc.get("version")), "filter": {}, "subscriptionEndpoints": [] } @@ -4026,39 +4064,30 @@ def update_email_notification(self, email_notification_params, email_notificatio if subs_endpoints: playbook_params["subscriptionEndpoints"] = subs_endpoints else: - playbook_params["subscriptionEndpoints"] = [] - fromEmailAddress = subs_endpoints_in_ccc.get('fromEmailAddress') - toEmailAddresses = subs_endpoints_in_ccc.get('toEmailAddresses') - subject = subs_endpoints_in_ccc.get('subject') - instance_name = subs_endpoints_in_ccc.get('name') - instance_description = subs_endpoints_in_ccc.get('description') - - temp_subscript_endpoint = { + playbook_params["subscriptionEndpoints"] = [{ "instanceId": instance_id, "subscriptionDetails": { "connectorType": "EMAIL" }, - "fromEmailAddress": fromEmailAddress, - "toEmailAddresses": toEmailAddresses, - "subject": subject, - "name": instance_name, - "description": instance_description - - } - playbook_params['subscriptionEndpoints'].append(temp_subscript_endpoint) + "fromEmailAddress": subs_endpoints_in_ccc.get('fromEmailAddress'), + "toEmailAddresses": subs_endpoints_in_ccc.get('toEmailAddresses'), + "subject": subs_endpoints_in_ccc.get('subject'), + "name": subs_endpoints_in_ccc.get('name'), + "description": subs_endpoints_in_ccc.get('description') + }] filter = email_notification_params.get("filter") ccc_filter = email_notification_in_ccc.get("filter") email_update_params = self.collect_notification_filter_params(playbook_params, filter, ccc_filter) - email_notification_update_params.append(email_update_params) + notification_update_params.append(email_update_params) try: - self.log("Requested payload for update_rest_email_event_subscription - {0}".format(str(email_notification_update_params)), "INFO") + self.log("Updating Email Event Notification '{0}' with following payload: {1}".format(name, str(notification_update_params)), "INFO") response = self.dnac._exec( family="event_management", function='update_email_event_subscription', op_modifies=True, - params={'payload': email_notification_update_params} + params={'payload': notification_update_params} ) time.sleep(2) self.log("Received API response from 'update_email_event_subscription': {0}".format(str(response)), "DEBUG") @@ -4088,8 +4117,7 @@ def update_email_notification(self, email_notification_params, email_notificatio except Exception as e: self.status = "failed" - self.msg = """Error while updating the Email Event Subscription Notification with name '{0}' in Cisco Catalyst - Center: {1}""".format(name, str(e)) + self.msg = "An error occurred while updating Email Event Subscription Notification '{0}': {1}".format(name, str(e)) self.log(self.msg, "ERROR") return self @@ -4194,8 +4222,10 @@ def get_diff_merged(self, config): if webhook_params.get('method') not in ["POST", "PUT"]: self.status = "failed" - self.msg = """Invalid Webhook method name '{0}' for creating/updating Webhook destination in Cisco Catalyst Center. - Select one of the following method 'POST/PUT'.""".format(webhook_params.get('method')) + self.msg = ( + "Invalid Webhook method name '{0}' for creating/updating Webhook destination in Cisco Catalyst Center." + "Select one of the following method 'POST/PUT'.".format(webhook_params.get('method')) + ) self.log(self.msg, "ERROR") return self @@ -4218,8 +4248,10 @@ def get_diff_merged(self, config): # Check if the input string matches the pattern if url and not regex_pattern.match(url): self.status = "failed" - self.msg = """Given url '{0}' is invalid url for Creating/Updating Webhook destination. It must starts with 'https://' and - follow the valid https url format.""".format(url) + self.msg = ( + "Given url '{0}' is invalid url for Creating/Updating Webhook destination. It must starts with " + "'https://' and follow the valid https url format.".format(url) + ) self.log(self.msg, "ERROR") return self @@ -4257,25 +4289,23 @@ def get_diff_merged(self, config): if server_address and re.search(special_chars, server_address): self.status = "failed" - self.msg = """Invalid Primary SMTP server hostname '{0}' as special character present in the input server address so - unable to add/update the email destination in Cisco Catalyst Center.""".format(server_address) + self.msg = ( + "Invalid Primary SMTP server hostname '{0}' as special character present in the input server " + "address so unable to add/update the email destination in CCC".format(server_address) + ) self.log(self.msg, "ERROR") return self if not self.have.get('email_destination'): - # Need to Add snmp destination in Cisco Catalyst Center with given playbook params - required_params = ['fromEmail', 'toEmail', 'subject'] + # Need to Add email destination in Cisco Catalyst Center with given playbook params invalid_email_params = [] - for item in required_params: - if not email_params[item]: - invalid_email_params.append(item) - if email_params.get('primarySMTPConfig') and not email_params.get('primarySMTPConfig').get('hostName'): - invalid_email_params.append('server_address') - if invalid_email_params: + if email_params.get('primarySMTPConfig') and not email_params.get('primarySMTPConfig').get('hostName'): self.status = "failed" - self.msg = """Required parameter '{0}' for configuring Email Destination in Cisco Catalyst Center - is missing.""".format(str(invalid_email_params)) + self.msg = ( + "Required parameter '{0}' for configuring Email Destination in Cisco Catalyst Center " + "is missing.".format(str(invalid_email_params)) + ) self.log(self.msg, "ERROR") self.result['response'] = self.msg return self @@ -4445,8 +4475,10 @@ def get_diff_merged(self, config): if not connection_setting: invalid_itsm_params.extends(["url", "username", "password"]) self.status = "failed" - self.msg = """Required parameter '{0}' for configuring ITSM Intergartion setting in Cisco Catalyst - Center is missing.""".format(str(invalid_itsm_params)) + self.msg = ( + "Required parameter '{0}' for configuring ITSM Intergartion setting in Cisco Catalyst " + "is missing.".format(str(invalid_itsm_params)) + ) self.log(self.msg, "ERROR") self.result['response'] = self.msg return self @@ -4458,8 +4490,10 @@ def get_diff_merged(self, config): if invalid_itsm_params: self.status = "failed" - self.msg = """Required parameter '{0}' for configuring ITSM Intergartion setting in Cisco Catalyst - Center is missing.""".format(str(invalid_itsm_params)) + self.msg = ( + "Required parameter '{0}' for configuring ITSM Intergartion setting in Cisco Catalyst " + "is missing.".format(str(invalid_itsm_params)) + ) self.log(self.msg, "ERROR") self.result['response'] = self.msg return self @@ -4500,8 +4534,10 @@ def get_diff_merged(self, config): if not notification_name: self.status = "failed" - self.msg = """Name is required parameter for creating/updating webhook events subscription notification - in Cisco Catalyst Center.""" + self.msg = ( + "Name is required parameter for creating/updating webhook events subscription notification" + "in Cisco Catalyst Center." + ) self.log(self.msg, "ERROR") return self @@ -4524,8 +4560,8 @@ def get_diff_merged(self, config): self.create_webhook_notification(webhook_notification_params).check_return_status() else: # Check whether the webhook evenet notification needs any update or not. - webhook_notification_need_update = self.webhook_notification_needs_update(webhook_notification_params, webhook_notification_in_ccc) - if not webhook_notification_need_update: + notification_update = self.webhook_notification_needs_update(webhook_notification_params, webhook_notification_in_ccc) + if not notification_update: self.msg = "Webhook Notification with name '{0}' needs no update in Cisco Catalyst Center".format(notification_name) self.log(self.msg, "INFO") self.result['changed'] = False @@ -4541,8 +4577,10 @@ def get_diff_merged(self, config): if not notification_name: self.status = "failed" - self.msg = """Name is required parameter for creating/updating Email events subscription notification - in Cisco Catalyst Center.""" + self.msg = ( + "Name is required parameter for creating/updating Email events subscription notification" + "in Cisco Catalyst Center." + ) self.log(self.msg, "ERROR") return self @@ -4565,9 +4603,9 @@ def get_diff_merged(self, config): self.create_email_notification(email_notification_params).check_return_status() else: # Check whether the email evenet notification needs any update or not. - email_notification_need_update = self.email_notification_needs_update(email_notification_params, email_notification_in_ccc) + notification_update = self.email_notification_needs_update(email_notification_params, email_notification_in_ccc) - if not email_notification_need_update: + if not notification_update: self.msg = "Email Notification with name '{0}' needs no update in Cisco Catalyst Center".format(notification_name) self.log(self.msg, "INFO") self.result['changed'] = False @@ -4583,8 +4621,10 @@ def get_diff_merged(self, config): if not notification_name: self.status = "failed" - self.msg = """Name is required parameter for creating/updating syslog events subscription notification - in Cisco Catalyst Center.""" + self.msg = ( + "Name is required parameter for creating/updating Syslog events subscription notification" + "in Cisco Catalyst Center." + ) self.log(self.msg, "ERROR") return self @@ -4607,8 +4647,8 @@ def get_diff_merged(self, config): self.create_syslog_notification(syslog_notification_params).check_return_status() else: # Check whether the syslog evenet notification needs any update or not. - sys_notification_need_update = self.syslog_notification_needs_update(syslog_notification_params, syslog_notification_in_ccc) - if not sys_notification_need_update: + sys_notification_update = self.syslog_notification_needs_update(syslog_notification_params, syslog_notification_in_ccc) + if not sys_notification_update: self.msg = "Syslog Notification with name '{0}' needs no update in Cisco Catalyst Center".format(notification_name) self.log(self.msg, "INFO") self.result['changed'] = False @@ -4700,8 +4740,10 @@ def get_diff_deleted(self, config): if not current_webhook_notifications: self.status = "success" self.result['changed'] = False - self.msg = """There is no Webhook Event Subscription Notification with name '{0}' present in Cisco Catalyst Center - so cannot delete the notification.""".format(webhook_notification_name) + self.msg = ( + "There is no Webhook Event Subscription Notification with name '{0}' present in in Cisco Catalyst Center " + "so cannot delete the notification.".format(webhook_notification_name) + ) self.log(self.name, "INFO") return self @@ -4713,8 +4755,10 @@ def get_diff_deleted(self, config): if webhook_notification_id: self.delete_events_subscription_notification(webhook_notification_id, webhook_notification_name).check_return_status() else: - self.msg = """Unable to delete Webhook Event Subscription Notification with name '{0}' as it is not present in Cisco - Catalyst Center""".format(webhook_notification_name) + self.msg = ( + "Unable to delete Webhook Event Subscription Notification with name '{0}' as it is not present in " + "Cisco Catalyst Center.".format(webhook_notification_name) + ) self.log(self.msg, "INFO") self.result['changed'] = False self.result['response'] = self.msg @@ -4729,8 +4773,10 @@ def get_diff_deleted(self, config): if not current_email_notifications: self.status = "success" self.result['changed'] = False - self.msg = """There is no email Event Subscription Notification with name '{0}' present in Cisco Catalyst Center - so cannot delete the notification.""".format(email_notification_name) + self.msg = ( + "There is no Email Event Subscription Notification with name '{0}' present in in Cisco Catalyst Center " + "so cannot delete the notification.".format(email_notification_name) + ) self.log(self.name, "INFO") return self @@ -4742,8 +4788,10 @@ def get_diff_deleted(self, config): if email_notification_id: self.delete_events_subscription_notification(email_notification_id, email_notification_name).check_return_status() else: - self.msg = """Unable to delete Email Event Subscription Notification with name '{0}' as it is not present in Cisco - Catalyst Center""".format(email_notification_name) + self.msg = ( + "Unable to delete Email Event Subscription Notification with name '{0}' as it is not present in " + "Cisco Catalyst Center.".format(email_notification_name) + ) self.log(self.msg, "INFO") self.result['changed'] = False self.result['response'] = self.msg @@ -4758,8 +4806,10 @@ def get_diff_deleted(self, config): if not current_syslog_notifications: self.status = "success" self.result['changed'] = False - self.msg = """There is no Syslog Event Subscription Notification with name '{0}' present in Cisco Catalyst Center - so cannot delete the notification.""".format(syslog_notification_name) + self.msg = ( + "There is no Syslog Event Subscription Notification with name '{0}' present in in Cisco Catalyst Center " + "so cannot delete the notification.".format(syslog_notification_name) + ) self.log(self.name, "INFO") return self @@ -4771,8 +4821,10 @@ def get_diff_deleted(self, config): if syslog_notification_id: self.delete_events_subscription_notification(syslog_notification_id, syslog_notification_name).check_return_status() else: - self.msg = """Unable to delete Syslog Event Subscription Notification with name '{0}' as it is not present in Cisco - Catalyst Center""".format(syslog_notification_name) + self.msg = ( + "Unable to delete Syslog Event Subscription Notification with name '{0}' as it is not present in " + "Cisco Catalyst Center.".format(syslog_notification_name) + ) self.log(self.msg, "INFO") self.result['changed'] = False self.result['response'] = self.msg From b53020c6c7ee0c338438572443bda675a2d0cb51 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 7 Jun 2024 11:24:52 +0530 Subject: [PATCH 72/78] Rename input parameter like events_name -> events, site_names->sites, destination_name->destination etc. and updated the documentation and examples as well rename in the playbook. --- ...nts_and_notifications_workflow_manager.yml | 18 +- ...ents_and_notifications_workflow_manager.py | 335 +++++++++--------- 2 files changed, 175 insertions(+), 178 deletions(-) diff --git a/playbooks/events_and_notifications_workflow_manager.yml b/playbooks/events_and_notifications_workflow_manager.yml index efd32b883e..74874dd52a 100644 --- a/playbooks/events_and_notifications_workflow_manager.yml +++ b/playbooks/events_and_notifications_workflow_manager.yml @@ -63,25 +63,25 @@ webhook_event_notification: name: "{{item.webhook_event_notification.name}}" description: "{{item.webhook_event_notification.description}}" - site_names: "{{item.webhook_event_notification.site_names}}" - events_names: "{{item.webhook_event_notification.events_names}}" - destination_name: "{{item.webhook_event_notification.destination_name}}" + sites: "{{item.webhook_event_notification.sites}}" + events: "{{item.webhook_event_notification.events}}" + destination: "{{item.webhook_event_notification.destination}}" email_event_notification: name: "{{item.email_event_notification.name}}" description: "{{item.email_event_notification.description}}" - site_names: "{{item.email_event_notification.site_names}}" - events_names: "{{item.email_event_notification.events_names}}" + sites: "{{item.email_event_notification.sites}}" + events: "{{item.email_event_notification.events}}" sender_email: "{{item.email_event_notification.sender_email}}" recipient_emails: "{{item.email_event_notification.recipient_emails}}" subject: "{{item.email_event_notification.subject}}" - instance_name: "{{item.email_event_notification.instance_name}}" + instance: "{{item.email_event_notification.instance}}" instance_description: "{{item.email_event_notification.instance_description}}" syslog_event_notification: name: "{{item.syslog_event_notification.name}}" description: "{{item.syslog_event_notification.description}}" - site_names: "{{item.syslog_event_notification.site_names}}" - events_names: "{{item.syslog_event_notification.events_names}}" - destination_name: "{{item.syslog_event_notification.destination_name}}" + sites: "{{item.syslog_event_notification.sites}}" + events: "{{item.syslog_event_notification.events}}" + destination: "{{item.syslog_event_notification.destination}}" with_items: "{{ events_notification }}" tags: diff --git a/plugins/modules/events_and_notifications_workflow_manager.py b/plugins/modules/events_and_notifications_workflow_manager.py index 14c85e8617..c31367b535 100644 --- a/plugins/modules/events_and_notifications_workflow_manager.py +++ b/plugins/modules/events_and_notifications_workflow_manager.py @@ -311,20 +311,20 @@ version: description: Version label for the event subscription, helping track updates or changes. type: str - destination_name: - description: The single Webhook destination's name for sending event notifications. + destination: + description: The name of the destination for sending event notifications via webhook. type: str required: True - events_names: - description: List of the event names to be subscribed while configuring notification (For example, ["AP Flap", "AP Reboot Crash"]). + events: + description: List of event names to be subscribed to for notification configurations (e.g., ["AP Flap", "AP Reboot Crash"]). type: list elements: str required: True - domain_name: - description: Broad category or domain under which the events fall (e.g., Know Your Network, Connectivity etc.). + domain: + description: The main category or domain under which events fall (e.g., Know Your Network, Connectivity, etc.). type: str - subdomain_names: - description: More specific categories within the main domain to further classify the events (e.g., ["Wireless", "Applications"]). + subdomains: + description: More specific categories within the main domain to further classify events (e.g., ["Wireless", "Applications"]). type: list elements: str event_types: @@ -332,21 +332,20 @@ type: list elements: str event_categories: - description: List of event categories to be included in the subscription of a notification - (For example, ["WARN", "INFO", "ERROR", "ALERT", "TASK_COMPLETE", "TASK_FAILURE"]). + description: List of event categories to be included in the subscription for notifications + (e.g., WARN, INFO, ERROR, ALERT, TASK_COMPLETE, TASK_FAILURE). type: list elements: str event_severities: - description: List of event severities to be included in the subscription of a notification (For example, ["1", "2", "3"]). + description: List of event severities to be included in the subscription for notifications (e.g., ["1", "2", "3"]). type: list elements: str event_sources: - description: List of event sources to be included in the subscription of a notification. + description: List of event sources to be included in the subscription for notifications. type: list elements: str - site_names: - description: List of site names where events are to be included in the subscription of a notification. - (For example, ["Global/India", "Global/USA"]). + sites: + description: List of site names where events are included in the notification subscription(e.g., ["Global/India", "Global/USA"]). type: list elements: str email_event_notification: @@ -365,8 +364,8 @@ version: description: Version label for the event subscription, helping track updates or changes. type: str - events_names: - description: List of the event names to be subscribed while configuring notification (For example, ["AP Flap", "AP Reboot Crash"]). + events: + description: List of event names to be subscribed to for notification configurations (e.g., ["AP Flap", "AP Reboot Crash"]). type: list elements: str required: True @@ -383,7 +382,7 @@ description: The Subject line for the email notification, briefly indicating the notification content. type: str required: True - instance_name: + instance: description: Name assigned to the specific email instance used for sending the notification. type: str required: True @@ -391,11 +390,11 @@ description: Detailed explanation of the email instance's purpose and how it relates to the notifications. type: str required: True - domain_name: - description: Broad category or domain under which the events fall (e.g., Know Your Network, Connectivity etc.). + domain: + description: The main category or domain under which events fall (e.g., Know Your Network, Connectivity, etc.). type: str - subdomain_names: - description: More specific categories within the main domain to further classify the events (e.g., ["Wireless", "Applications"]). + subdomains: + description: More specific categories within the main domain to further classify events (e.g., ["Wireless", "Applications"]). type: list elements: str event_types: @@ -403,21 +402,20 @@ type: list elements: str event_categories: - description: List of event categories to be included in the subscription of a notification - (For example, ["WARN", "INFO", "ERROR", "ALERT", "TASK_COMPLETE", "TASK_FAILURE"]). + description: List of event categories to be included in the subscription for notifications + (e.g., WARN, INFO, ERROR, ALERT, TASK_COMPLETE, TASK_FAILURE). type: list elements: str event_severities: - description: List of event severities to be included in the subscription of a notification (For example, ["1", "2", "3"]). + description: List of event severities to be included in the subscription for notifications (e.g., ["1", "2", "3"]). type: list elements: str event_sources: - description: List of event sources to be included in the subscription of a notification. + description: List of event sources to be included in the subscription for notifications. type: list elements: str - site_names: - description: List of site names where events are to be included in the subscription of a notification. - (For example, ["Global/India", "Global/USA"]). + sites: + description: List of site names where events are included in the notification subscription(e.g., ["Global/India", "Global/USA"]). type: list elements: str syslog_event_notification: @@ -436,20 +434,20 @@ version: description: Version label for the event subscription, helping track updates or changes. type: str - destination_name: - description: The single Syslog destination's name for sending event notifications. + destination: + description: The name of the destination for sending event notifications via syslog. type: str required: True - events_names: - description: List of the event names to be subscribed while configuring notification (For example, ["AP Flap", "AP Reboot Crash"]). + events: + description: List of event names to be subscribed to for notification configurations (e.g., ["AP Flap", "AP Reboot Crash"]). type: list elements: str required: True - domain_name: - description: Broad category or domain under which the events fall (e.g., Know Your Network, Connectivity etc.). + domain: + description: The main category or domain under which events fall (e.g., Know Your Network, Connectivity, etc.). type: str - subdomain_names: - description: More specific categories within the main domain to further classify the events (e.g., ["Wireless", "Applications"]). + subdomains: + description: More specific categories within the main domain to further classify events (e.g., ["Wireless", "Applications"]). type: list elements: str event_types: @@ -457,21 +455,20 @@ type: list elements: str event_categories: - description: List of event categories to be included in the subscription of a notification - (For example, ["WARN", "INFO", "ERROR", "ALERT", "TASK_COMPLETE", "TASK_FAILURE"]). + description: List of event categories to be included in the subscription for notifications + (e.g., WARN, INFO, ERROR, ALERT, TASK_COMPLETE, TASK_FAILURE). type: list elements: str event_severities: - description: List of event severities to be included in the subscription of a notification (For example, ["1", "2", "3"]). + description: List of event severities to be included in the subscription for notifications (e.g., ["1", "2", "3"]). type: list elements: str event_sources: - description: List of event sources to be included in the subscription of a notification. + description: List of event sources to be included in the subscription for notifications. type: list elements: str - site_names: - description: List of site names where events are to be included in the subscription of a notification. - (For example, ["Global/India", "Global/USA"]). + sites: + description: List of site names where events are included in the notification subscription(e.g., ["Global/India", "Global/USA"]). type: list elements: str @@ -739,9 +736,9 @@ - webhook_event_notification: name: "Webhook Notification." description: "Notification for webhook events subscription" - site_names: ["Global/India", "Global/USA"] - events_names: ["AP Flap", "AP Reboot Crash"] - destination_name: "Webhook Demo" + sites: ["Global/India", "Global/USA"] + events: ["AP Flap", "AP Reboot Crash"] + destination: "Webhook Demo" - name: Updating Webhook Notification with the list of names of subscribed events in the system. cisco.dnac.events_and_notifications_workflow_manager: @@ -759,8 +756,8 @@ - webhook_event_notification: name: "Webhook Notification." description: "Updated notification for webhook events subscription" - site_names: ["Global/India", "Global/USA", "Global/China"] - destination_name: "Webhook Demo" + sites: ["Global/India", "Global/USA", "Global/China"] + destination: "Webhook Demo" - name: Creating Email Notification with the list of names of subscribed events in the system. cisco.dnac.events_and_notifications_workflow_manager: @@ -778,12 +775,12 @@ - email_event_notification: name: "Email Notification" description: "Notification description for email subscription creation" - site_names: ["Global/India", "Global/USA"] - events_names: ["AP Flap", "AP Reboot Crash"] + sites: ["Global/India", "Global/USA"] + events: ["AP Flap", "AP Reboot Crash"] sender_email: "catalyst@cisco.com" recipient_emails: ["test@cisco.com", "demo@cisco.com"] subject: "Mail test" - instance_name: Email Instance test + instance: Email Instance test - name: Updating Email Notification with the list of names of subscribed events in the system. cisco.dnac.events_and_notifications_workflow_manager: @@ -801,12 +798,12 @@ - email_event_notification: name: "Email Notification" description: "Notification description for email subscription updation" - site_names: ["Global/India", "Global/USA"] - events_names: ["AP Flap", "AP Reboot Crash"] + sites: ["Global/India", "Global/USA"] + events: ["AP Flap", "AP Reboot Crash"] sender_email: "catalyst@cisco.com" recipient_emails: ["test@cisco.com", "demo@cisco.com", "update@cisco.com"] subject: "Mail test for updation" - instance_name: Email Instance test + instance: Email Instance test - name: Creating Syslog Notification with the list of names of subscribed events in the system. cisco.dnac.events_and_notifications_workflow_manager: @@ -824,9 +821,9 @@ - syslog_event_notification: name: "Syslog Notification." description: "Notification for syslog events subscription" - site_names: ["Global/India", "Global/USA"] - events_names: ["AP Flap", "AP Reboot Crash"] - destination_name: "Syslog Demo" + sites: ["Global/India", "Global/USA"] + events: ["AP Flap", "AP Reboot Crash"] + destination: "Syslog Demo" - name: Updating Syslog Notification with the list of names of subscribed events in the system. cisco.dnac.events_and_notifications_workflow_manager: @@ -844,8 +841,8 @@ - syslog_event_notification: name: "Syslog Notification." description: "Updated notification for syslog events subscription" - site_names: ["Global/India", "Global/USA", "Global/China"] - events_names: ["AP Flap", "AP Reboot Crash"] + sites: ["Global/India", "Global/USA", "Global/China"] + events: ["AP Flap", "AP Reboot Crash"] - name: Deleting ITSM Integration Setting with given name from the system. cisco.dnac.events_and_notifications_workflow_manager: @@ -1042,11 +1039,11 @@ def validate_input(self): 'name': {'type': 'str'}, 'version': {'type': 'str'}, 'description': {'type': 'str'}, - 'site_names': {'type': 'list', 'elements': 'str'}, - 'events_names': {'type': 'list', 'elements': 'str'}, - 'destination_name': {'type': 'str'}, - 'domain_name': {'type': 'str'}, - 'subdomain_names': {'type': 'list', 'elements': 'str'}, + 'sites': {'type': 'list', 'elements': 'str'}, + 'events': {'type': 'list', 'elements': 'str'}, + 'destination': {'type': 'str'}, + 'domain': {'type': 'str'}, + 'subdomains': {'type': 'list', 'elements': 'str'}, 'event_types': {'type': 'list', 'elements': 'str'}, 'event_categories': {'type': 'list', 'elements': 'str'}, 'event_severities': {'type': 'list', 'elements': 'str'}, @@ -1057,15 +1054,15 @@ def validate_input(self): 'name': {'type': 'str'}, 'version': {'type': 'str'}, 'description': {'type': 'str'}, - 'site_names': {'type': 'list', 'elements': 'str'}, - 'events_names': {'type': 'list', 'elements': 'str'}, + 'sites': {'type': 'list', 'elements': 'str'}, + 'events': {'type': 'list', 'elements': 'str'}, 'sender_email': {'type': 'str'}, 'recipient_emails': {'type': 'list', 'elements': 'str'}, 'subject': {'type': 'str'}, - 'instance_name': {'type': 'str'}, + 'instance': {'type': 'str'}, 'instance_description': {'type': 'str'}, - 'domain_name': {'type': 'str'}, - 'subdomain_names': {'type': 'list', 'elements': 'str'}, + 'domain': {'type': 'str'}, + 'subdomains': {'type': 'list', 'elements': 'str'}, 'event_types': {'type': 'list', 'elements': 'str'}, 'event_categories': {'type': 'list', 'elements': 'str'}, 'event_severities': {'type': 'list', 'elements': 'str'}, @@ -1076,11 +1073,11 @@ def validate_input(self): 'name': {'type': 'str'}, 'version': {'type': 'str'}, 'description': {'type': 'str'}, - 'site_names': {'type': 'list', 'elements': 'str'}, - 'events_names': {'type': 'list', 'elements': 'str'}, - 'destination_name': {'type': 'str'}, - 'domain_name': {'type': 'str'}, - 'subdomain_names': {'type': 'list', 'elements': 'str'}, + 'sites': {'type': 'list', 'elements': 'str'}, + 'events': {'type': 'list', 'elements': 'str'}, + 'destination': {'type': 'str'}, + 'domain': {'type': 'str'}, + 'subdomains': {'type': 'list', 'elements': 'str'}, 'event_types': {'type': 'list', 'elements': 'str'}, 'event_categories': {'type': 'list', 'elements': 'str'}, 'event_severities': {'type': 'list', 'elements': 'str'}, @@ -2632,18 +2629,18 @@ def get_syslog_notification_details(self): self.log(self.msg, "ERROR") self.check_return_status() - def get_syslog_subscription_detail(self, destination_name): + def get_syslog_subscription_detail(self, destination): """ Retrieves the details of a specific Syslog destination subscription from the Cisco Catalyst Center. Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - destination_name (str): The name of the Syslog destination for which details needs to be fetched. + destination (str): The name of the Syslog destination for which details needs to be fetched. Returns: dict or list: A dictionary containing the details of the Syslog destination subscription if found. Returns an empty list if no destination is found or if an error occurs during the API call. Description: This function calls an API to fetch the details of all Syslog destination from the Cisco Catalyst Center. - It then searches for a subscription that matches the given `destination_name`. If a match is found, it returns + It then searches for a subscription that matches the given `destination`. If a match is found, it returns details of the matching subscription. If no match is found or if an error occurs, it logs the appropriate message and handles the exception accordingly. """ @@ -2661,9 +2658,9 @@ def get_syslog_subscription_detail(self, destination_name): return sys_destination_details for dest in response: - if dest["name"] == destination_name: + if dest["name"] == destination: return dest - self.log("Syslog destination with the name '{0}' not found in Cisco Catalyst Center.".format(destination_name), "INFO") + self.log("Syslog destination with the name '{0}' not found in Cisco Catalyst Center.".format(destination), "INFO") return sys_destination_details @@ -2671,17 +2668,17 @@ def get_syslog_subscription_detail(self, destination_name): self.status = "failed" self.msg = ( "Error while getting the details of the Syslog Subscription with the given name '{0}'" - " from Cisco Catalyst Center: {1}".format(destination_name, repr(e)) + " from Cisco Catalyst Center: {1}".format(destination, repr(e)) ) self.log(self.msg, "ERROR") self.check_return_status() - def get_event_ids(self, events_names): + def get_event_ids(self, events): """ Retrieves the event IDs for a given list of event names from the Cisco Catalyst Center. Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - events_names (list of str): A list of event names for which the event IDs need to be retrieved. + events (list of str): A list of event names for which the event IDs need to be retrieved. Returns: list of str: A list of event IDs corresponding to the provided event names. If an event name is not found, it is skipped. @@ -2694,7 +2691,7 @@ def get_event_ids(self, events_names): event_ids = [] - for event_name in events_names: + for event_name in events: try: response = self.dnac._exec( family="event_management", @@ -2721,12 +2718,12 @@ def get_event_ids(self, events_names): return event_ids - def get_site_ids(self, site_names): + def get_site_ids(self, sites): """ Retrieves the site IDs for a given list of site names from the Cisco Catalyst Center. Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - site_names (list of str): A list of site names for which the site IDs need to be retrieved. + sites (list of str): A list of site names for which the site IDs need to be retrieved. Returns: list of str: A list of site IDs corresponding to the provided site names. If a site name is not found, it is skipped and return empty list. @@ -2738,7 +2735,7 @@ def get_site_ids(self, site_names): """ site_ids = [] - for site in site_names: + for site in sites: try: response = self.dnac._exec( family="sites", @@ -2792,15 +2789,15 @@ def collect_syslog_notification_playbook_params(self, syslog_notification_detail } # Collect the Instance ID of the syslog destination self.log("Collecting parameters for Syslog Event Notification named '{0}'.".format(name), "INFO") - destination_name = syslog_notification_details.get('destination_name') + destination = syslog_notification_details.get('destination') - if destination_name: - subscription_details = self.get_syslog_subscription_detail(destination_name) + if destination: + subscription_details = self.get_syslog_subscription_detail(destination) if not subscription_details: self.status = "failed" self.msg = """Unable to create/update the syslog event notification '{0}' as syslog desination '{1}' is not configured or - present in Cisco Catalyst Center""".format(name, destination_name) + present in Cisco Catalyst Center""".format(name, destination) self.log(self.msg, "ERROR") self.check_return_status() @@ -2814,27 +2811,27 @@ def collect_syslog_notification_playbook_params(self, syslog_notification_detail } playbook_params["subscriptionEndpoints"].append(temp_subscript_endpoint) - events_names = syslog_notification_details.get('events_names') - if events_names: - events_ids = self.get_event_ids(events_names) + events = syslog_notification_details.get('events') + if events: + events_ids = self.get_event_ids(events) if not events_ids: self.status = "failed" self.msg = ( "Unable to create/update Syslog event notification as the given event names '{0}' " "are incorrect or could not be found." - ).format(str(events_names)) + ).format(str(events)) self.log(self.msg, "ERROR") self.check_return_status() playbook_params["filter"]["eventIds"] = events_ids - domain_name = syslog_notification_details.get("domain_name") - subdomain_names = syslog_notification_details.get("subdomain_names") - if domain_name and subdomain_names: + domain = syslog_notification_details.get("domain") + subdomains = syslog_notification_details.get("subdomains") + if domain and subdomains: playbook_params["filter"]["domainsSubdomains"] = [] domain_dict = { - "domain": domain_name, - "subDomains": subdomain_names + "domain": domain, + "subDomains": subdomains } playbook_params["filter"]["domainsSubdomains"].append(domain_dict) @@ -2852,11 +2849,11 @@ def collect_syslog_notification_playbook_params(self, syslog_notification_detail if value: playbook_params["filter"][filter_mapping[key]] = value - site_names = syslog_notification_details.get("site_names") - if site_names: - site_ids = self.get_site_ids(site_names) + sites = syslog_notification_details.get("sites") + if sites: + site_ids = self.get_site_ids(sites) if not site_ids: - self.msg = "Unable to find the Site ID's for the given site(s) - '{0}' in the playbook's input.".format(site_names) + self.msg = "Unable to find the Site ID's for the given site(s) - '{0}' in the playbook's input.".format(sites) self.log(self.msg, "INFO") playbook_params["filter"]["siteIds"] = site_ids @@ -2893,12 +2890,12 @@ def mandatory_syslog_notification_parameter_check(self, syslog_notification_para subs_endpoints = syslog_notification_params.get('subscriptionEndpoints') if not subs_endpoints: - required_params_absent.append("destination_name") + required_params_absent.append("destination") filters = syslog_notification_params.get("filter") if not filters.get("eventIds"): - required_params_absent.append("events_names") + required_params_absent.append("events") if required_params_absent: self.status = "failed" @@ -3240,18 +3237,18 @@ def get_webhook_notification_details(self): self.log(self.msg, "ERROR") self.check_return_status() - def get_webhook_subscription_detail(self, destination_name): + def get_webhook_subscription_detail(self, destination): """ Retrieves the details of a specific webhook destination subscription from the Cisco Catalyst Center. Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - destination_name (str): The name of the webhook destination for which details needs to be fetched. + destination (str): The name of the webhook destination for which details needs to be fetched. Returns: dict or list: A dictionary containing the details of the webhook destination subscription if found. Returns an empty list if no destination is found or if an error occurs during the API call. Description: This function calls an API to fetch the details of all webhook destination from the Cisco Catalyst Center. - It then searches for a subscription that matches the given `destination_name`. If a match is found, it returns + It then searches for a subscription that matches the given `destination`. If a match is found, it returns details of the matching subscription. If no match is found or if an error occurs, it logs the appropriate message and handles the exception accordingly. """ @@ -3269,16 +3266,16 @@ def get_webhook_subscription_detail(self, destination_name): return web_destination_details for dest in response: - if dest["name"] == destination_name: + if dest["name"] == destination: return dest - self.log("There is no webhook destination with given name '{0}' present in Cisco Catalyst Center.".format(destination_name), "INFO") + self.log("There is no webhook destination with given name '{0}' present in Cisco Catalyst Center.".format(destination), "INFO") return web_destination_details except Exception as e: self.status = "failed" self.msg = """Error while getting the details of webhook Subscription with given name '{0}' present in - Cisco Catalyst Center: {1}""".format(destination_name, str(e)) + Cisco Catalyst Center: {1}""".format(destination, str(e)) self.log(self.msg, "ERROR") self.check_return_status() @@ -3310,15 +3307,15 @@ def collect_webhook_notification_playbook_params(self, webhook_notification_deta } # Collect the Instance ID of the webhook destination self.log("Collecting parameters for Webhook Event Notification named '{0}'.".format(name), "INFO") - destination_name = webhook_notification_details.get('destination_name') + destination = webhook_notification_details.get('destination') - if destination_name: - subscription_details = self.get_webhook_subscription_detail(destination_name) + if destination: + subscription_details = self.get_webhook_subscription_detail(destination) if not subscription_details: self.status = "failed" self.msg = """Unable to create/update the webhook event notification '{0}' as webhook desination '{1}' is not configured or - present in Cisco Catalyst Center""".format(name, destination_name) + present in Cisco Catalyst Center""".format(name, destination) self.log(self.msg, "ERROR") self.check_return_status() @@ -3332,27 +3329,27 @@ def collect_webhook_notification_playbook_params(self, webhook_notification_deta } playbook_params["subscriptionEndpoints"].append(temp_subscript_endpoint) - events_names = webhook_notification_details.get('events_names') - if events_names: - events_ids = self.get_event_ids(events_names) + events = webhook_notification_details.get('events') + if events: + events_ids = self.get_event_ids(events) if not events_ids: self.status = "failed" self.msg = ( "Unable to create/update Webhook event notification as the given event names '{0}' " "are incorrect or could not be found." - ).format(str(events_names)) + ).format(str(events)) self.log(self.msg, "ERROR") self.check_return_status() playbook_params["filter"]["eventIds"] = events_ids - domain_name = webhook_notification_details.get("domain_name") - subdomain_names = webhook_notification_details.get("subdomain_names") - if domain_name and subdomain_names: + domain = webhook_notification_details.get("domain") + subdomains = webhook_notification_details.get("subdomains") + if domain and subdomains: playbook_params["filter"]["domainsSubdomains"] = [] domain_dict = { - "domain": domain_name, - "subDomains": subdomain_names + "domain": domain, + "subDomains": subdomains } playbook_params["filter"]["domainsSubdomains"].append(domain_dict) @@ -3370,15 +3367,15 @@ def collect_webhook_notification_playbook_params(self, webhook_notification_deta if value: playbook_params["filter"][filter_mapping[key]] = value - site_names = webhook_notification_details.get("site_names") - if site_names: - site_ids = self.get_site_ids(site_names) + sites = webhook_notification_details.get("sites") + if sites: + site_ids = self.get_site_ids(sites) if not site_ids: - self.msg = "Unable to find the Site IDs for the given site(s) - '{0}' in the playbook's input.".format(site_names) + self.msg = "Unable to find the Site IDs for the given site(s) - '{0}' in the playbook's input.".format(sites) self.log(self.msg, "INFO") playbook_params["filter"]["siteIds"] = site_ids - self.log("Site IDs '{0}' found for site names '{1}'. Added to filter.".format(site_ids, site_names), "INFO") + self.log("Site IDs '{0}' found for site names '{1}'. Added to filter.".format(site_ids, sites), "INFO") self.log("Webhook notification playbook parameters collected successfully for '{0}': {1}".format(name, playbook_params), "INFO") webhook_notification_params.append(playbook_params) @@ -3413,10 +3410,10 @@ def mandatory_webhook_notification_parameter_check(self, webhook_notification_pa required_params_absent.append("description") if not subs_endpoints: - required_params_absent.append("destination_name") + required_params_absent.append("destination") if not filters.get("eventIds"): - required_params_absent.append("events_names") + required_params_absent.append("events") if required_params_absent: self.status = "failed" @@ -3655,18 +3652,18 @@ def get_email_notification_details(self): self.log(self.msg, "ERROR") self.check_return_status() - def get_email_subscription_detail(self, instance_name): + def get_email_subscription_detail(self, instance): """ Retrieves the details of a specific email destination subscription from the Cisco Catalyst Center. Args: self (object): An instance of a class used for interacting with Cisco Catalyst Center. - instance_name (str): The name of the email destination for which details needs to be fetched. + instance (str): The name of the email destination for which details needs to be fetched. Returns: dict or list: A dictionary containing the details of the email destination subscription if found. Returns an empty list if no destination is found or if an error occurs during the API call. Description: This function calls an API to fetch the details of all email destination from the Cisco Catalyst Center. - It then searches for a subscription that matches the given `instance_name`. If a match is found, it returns + It then searches for a subscription that matches the given `instance`. If a match is found, it returns details of the matching subscription. If no match is found or if an error occurs, it logs the appropriate message and handles the exception accordingly. """ @@ -3684,16 +3681,16 @@ def get_email_subscription_detail(self, instance_name): return email_destination_details for dest in response: - if dest["name"] == instance_name: + if dest["name"] == instance: return dest - self.log("There is no email destination with given name '{0}' present in Cisco Catalyst Center.".format(instance_name), "INFO") + self.log("There is no email destination with given name '{0}' present in Cisco Catalyst Center.".format(instance), "INFO") return email_destination_details except Exception as e: self.status = "failed" self.msg = """Error while getting the details of Email event Subscription with given destination name '{0}' present in - Cisco Catalyst Center: {1}""".format(instance_name, str(e)) + Cisco Catalyst Center: {1}""".format(instance, str(e)) self.log(self.msg, "ERROR") self.check_return_status() @@ -3725,15 +3722,15 @@ def collect_email_notification_playbook_params(self, email_notification_details) } # Collect the Instance ID of the email destination self.log("Collecting parameters for Email Event Notification named '{0}'.".format(email_notf_name), "INFO") - instance_name = email_notification_details.get('instance_name') + instance = email_notification_details.get('instance') - if not instance_name: + if not instance: self.status = "failed" self.msg = "Instance name for Subscription Endpoints is required for Email notification '{0}'.".format(email_notf_name) self.log(self.msg, "ERROR") return self - subscription_details = self.get_email_subscription_detail(instance_name) + subscription_details = self.get_email_subscription_detail(instance) instance_id = None if subscription_details: @@ -3769,35 +3766,35 @@ def collect_email_notification_playbook_params(self, email_notification_details) "fromEmailAddress": fromEmailAddress, "toEmailAddresses": toEmailAddresses, "subject": subject, - "name": instance_name, + "name": instance, "description": description } } playbook_params["subscriptionEndpoints"].append(temp_subscript_endpoint) else: - self.log("No subscription details found for instance '{0}'.".format(instance_name), "WARNING") + self.log("No subscription details found for instance '{0}'.".format(instance), "WARNING") - events_names = email_notification_details.get('events_names') - if events_names: - events_ids = self.get_event_ids(events_names) + events = email_notification_details.get('events') + if events: + events_ids = self.get_event_ids(events) if not events_ids: self.status = "failed" self.msg = ( "Unable to create/update Email event notification as the given event names '{0}' " "are incorrect or could not be found." - ).format(str(events_names)) + ).format(str(events)) self.log(self.msg, "ERROR") self.check_return_status() playbook_params["filter"]["eventIds"] = events_ids - domain_name = email_notification_details.get("domain_name") - subdomain_names = email_notification_details.get("subdomain_names") - if domain_name and subdomain_names: + domain = email_notification_details.get("domain") + subdomains = email_notification_details.get("subdomains") + if domain and subdomains: playbook_params["filter"]["domainsSubdomains"] = [] domain_dict = { - "domain": domain_name, - "subDomains": subdomain_names + "domain": domain, + "subDomains": subdomains } playbook_params["filter"]["domainsSubdomains"].append(domain_dict) @@ -3815,11 +3812,11 @@ def collect_email_notification_playbook_params(self, email_notification_details) if value: playbook_params["filter"][filter_mapping[key]] = value - site_names = email_notification_details.get("site_names") - if site_names: - site_ids = self.get_site_ids(site_names) + sites = email_notification_details.get("sites") + if sites: + site_ids = self.get_site_ids(sites) if not site_ids: - self.msg = "Unable to find the Site IDs for the given site(s) - '{0}' in the playbook's input.".format(site_names) + self.msg = "Unable to find the Site IDs for the given site(s) - '{0}' in the playbook's input.".format(sites) self.log(self.msg, "INFO") playbook_params["filter"]["siteIds"] = site_ids @@ -3860,7 +3857,7 @@ def mandatory_email_notification_parameter_check(self, email_notification_params subs_endpoints = email_notification_params.get('subscriptionEndpoints') if not subs_endpoints: - required_params_absent.extends(["instance_name", "fromEmailAddress", "toEmailAddresses", "subject"]) + required_params_absent.extends(["instance", "sender_email", "recipient_emails", "subject"]) else: subs_endpoints = subs_endpoints[0].get("subscriptionDetails") if not subs_endpoints.get("fromEmailAddress"): @@ -3870,11 +3867,11 @@ def mandatory_email_notification_parameter_check(self, email_notification_params if not subs_endpoints.get("subject"): required_params_absent.append("subject") if not subs_endpoints.get("name"): - required_params_absent.append("instance_name") + required_params_absent.append("instance") filters = email_notification_params.get("filter") if not filters: - required_params_absent.append("events_names") + required_params_absent.append("events") if required_params_absent: self.status = "failed" @@ -4204,9 +4201,9 @@ def get_diff_merged(self, config): # Create/Update Rest Webhook destination in Cisco Catalyst Center if config.get('webhook_destination'): webhook_details = self.want.get('webhook_details') - destination_name = webhook_details.get('name') + destination = webhook_details.get('name') - if not destination_name: + if not destination: self.status = "failed" self.msg = "Name is required parameter for adding/updating Webhook destination for creating/updating the event." self.log(self.msg, "ERROR") @@ -4214,7 +4211,7 @@ def get_diff_merged(self, config): is_destination_exist = False for webhook_dict in self.have.get('webhook_destinations'): - if webhook_dict['name'] == destination_name: + if webhook_dict['name'] == destination: webhook_dest_detail_in_ccc = webhook_dict is_destination_exist = True break @@ -4269,7 +4266,7 @@ def get_diff_merged(self, config): webhook_need_update = self.webhook_dest_needs_update(webhook_params, webhook_dest_detail_in_ccc) if not webhook_need_update: - self.msg = "Webhook Destination with name '{0}' needs no update in Cisco Catalyst Center".format(destination_name) + self.msg = "Webhook Destination with name '{0}' needs no update in Cisco Catalyst Center".format(destination) self.log(self.msg, "INFO") self.result['changed'] = False self.result['response'] = self.msg @@ -4386,9 +4383,9 @@ def get_diff_merged(self, config): # Create/Update snmp destination in Cisco Catalyst Center if config.get('snmp_destination'): snmp_details = self.want.get("snmp_details") - destination_name = snmp_details.get("name") + destination = snmp_details.get("name") - if not destination_name: + if not destination: self.status = "failed" self.msg = "Name is required parameter for adding/updating SNMP destination for creating/updating the event." self.log(self.msg, "ERROR") @@ -4396,7 +4393,7 @@ def get_diff_merged(self, config): is_destination_exist = False for snmp_dict in self.have.get('snmp_destinations'): - if snmp_dict['name'] == destination_name: + if snmp_dict['name'] == destination: snmp_dest_detail_in_ccc = snmp_dict is_destination_exist = True break @@ -4420,7 +4417,7 @@ def get_diff_merged(self, config): if privacy_type and privacy_type not in ["AES128", "DES"]: self.status = "failed" self.msg = """Invalid SNMP Privacy type '{0}' given in playbook. Select either AES128/DES as privacy type to add/update the snmp - destination '{1}' in the Cisco Catalyst Center.""".format(privacy_type, destination_name) + destination '{1}' in the Cisco Catalyst Center.""".format(privacy_type, destination) self.log(self.msg, "ERROR") return self @@ -4428,13 +4425,13 @@ def get_diff_merged(self, config): # Need to Add snmp destination in Cisco Catalyst Center with given playbook params self.check_snmp_required_parameters(snmp_params).check_return_status() self.log("""Required parameter validated successfully for adding SNMP Destination with name '{0}' in Cisco - Catalyst Center.""".format(destination_name), "INFO") + Catalyst Center.""".format(destination), "INFO") self.add_snmp_destination(snmp_params).check_return_status() else: # Check destination needs update and if yes then update SNMP Destination snmp_need_update = self.snmp_dest_needs_update(snmp_params, snmp_dest_detail_in_ccc) if not snmp_need_update: - self.msg = "SNMP Destination with name '{0}' needs no update in Cisco Catalyst Center".format(destination_name) + self.msg = "SNMP Destination with name '{0}' needs no update in Cisco Catalyst Center".format(destination) self.log(self.msg, "INFO") self.result['changed'] = False self.result['response'] = self.msg From 631bdf9157892e675361260de92c31d1e2a7ff9d Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 7 Jun 2024 11:32:28 +0530 Subject: [PATCH 73/78] Added the maximum timeout for checking the execution of task id and execution id --- plugins/module_utils/dnac.py | 24 +++++++++++++++---- .../network_settings_workflow_manager.py | 2 +- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index b378420177..0f817f3cb5 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -68,6 +68,7 @@ def __init__(self, module): 'parsed': self.verify_diff_parsed } self.dnac_log = dnac_params.get("dnac_log") + self.max_timeout = self.params.get('dnac_api_task_timeout') if self.dnac_log and not DnacBase.__is_log_init: self.dnac_log_level = dnac_params.get("dnac_log_level") or 'WARNING' @@ -331,7 +332,14 @@ def check_task_response_status(self, response, validation_string, data=False): return self task_id = response.get("taskId") + start_time = time.time() while True: + end_time = time.time() + if (end_time - start_time) >= self.max_timeout: + self.log("Max timeout of {0} sec has reached for the task id '{1}' for the event and unexpected " + "api status so moving out of the loop.".format(self.max_timeout, task_id), "WARNING") + break + task_details = self.get_task_details(task_id) self.log('Getting task details from task ID {0}: {1}'.format(task_id, task_details), "DEBUG") @@ -401,9 +409,16 @@ def check_execution_response_status(self, response): self.status = "failed" return self - executionid = response.get("executionId") + execution_id = response.get("executionId") + start_time = time.time() while True: - execution_details = self.get_execution_details(executionid) + end_time = time.time() + if (end_time - start_time) >= self.max_timeout: + self.log("Max timeout of {0} sec has reached for the execution id '{1}' for the event and unexpected " + "api status so moving out of the loop.".format(self.max_timeout, execution_id), "WARNING") + break + + execution_details = self.get_execution_details(execution_id) if execution_details.get("status") == "SUCCESS": self.result['changed'] = True self.msg = "Successfully executed" @@ -529,15 +544,14 @@ def check_status_api_events(self, status_execution_id): response from the API. If the timeout is reached first, the method logs a warning and returns None. """ - max_timeout = self.params.get('dnac_api_task_timeout') events_response = None start_time = time.time() while True: end_time = time.time() - if (end_time - start_time) >= max_timeout: + if (end_time - start_time) >= self.max_timeout: self.log("""Max timeout of {0} sec has reached for the execution id '{1}' for the event and unexpected - api status so moving out of the loop.""".format(max_timeout, status_execution_id), "WARNING") + api status so moving out of the loop.""".format(self.max_timeout, status_execution_id), "WARNING") break # Now we check the status of API Events for configuring destination and notifications response = self.dnac._exec( diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index 48c478a628..ff49b5bd9e 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -1540,7 +1540,7 @@ def get_have_network(self, network_details): site_id = self.get_site_id(site_name) if site_id is None: - self.msg = "Failed to get site id from {0}".format(site_name) + self.msg = "The site with the name '{0}' is not available in the DNAC".format(site_name) self.status = "failed" return self From 34aea4622a730033866d49392366b0a5553f5bd1 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 7 Jun 2024 12:02:39 +0530 Subject: [PATCH 74/78] Addressed the review comments --- plugins/module_utils/dnac.py | 10 +++++----- plugins/modules/network_settings_workflow_manager.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 0f817f3cb5..2472f192c1 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -336,8 +336,8 @@ def check_task_response_status(self, response, validation_string, data=False): while True: end_time = time.time() if (end_time - start_time) >= self.max_timeout: - self.log("Max timeout of {0} sec has reached for the task id '{1}' for the event and unexpected " - "api status so moving out of the loop.".format(self.max_timeout, task_id), "WARNING") + self.log("Max timeout of {0} sec has reached for the execution id '{1}'. Exiting the loop due to unexpected API status." + .format(self.max_timeout, task_id), "WARNING") break task_details = self.get_task_details(task_id) @@ -358,7 +358,7 @@ def check_task_response_status(self, response, validation_string, data=False): self.status = "success" break - self.log("progress set to {0} for taskid: {1}".format(task_details.get('progress'), task_id), "DEBUG") + self.log("Progress is {0} for task ID: {1}".format(task_details.get('progress'), task_id), "DEBUG") return self @@ -414,8 +414,8 @@ def check_execution_response_status(self, response): while True: end_time = time.time() if (end_time - start_time) >= self.max_timeout: - self.log("Max timeout of {0} sec has reached for the execution id '{1}' for the event and unexpected " - "api status so moving out of the loop.".format(self.max_timeout, execution_id), "WARNING") + self.log("Max timeout of {0} sec has reached for the execution id '{1}'. Exiting the loop due to unexpected API status." + .format(self.max_timeout, execution_id), "WARNING") break execution_details = self.get_execution_details(execution_id) diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index ff49b5bd9e..b1f4e9d1a8 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -1540,7 +1540,7 @@ def get_have_network(self, network_details): site_id = self.get_site_id(site_name) if site_id is None: - self.msg = "The site with the name '{0}' is not available in the DNAC".format(site_name) + self.msg = "The site with the name '{0}' is not available in the Catalyst Center".format(site_name) self.status = "failed" return self From 5b456e1c3a68e82195978bee06365fd8b3697a7d Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 7 Jun 2024 12:50:56 +0530 Subject: [PATCH 75/78] Added the API name in check_task_response_status and check_execution_response_status --- plugins/module_utils/dnac.py | 19 ++++++++++++------- .../device_credential_workflow_manager.py | 6 +++--- ...ise_radius_integration_workflow_manager.py | 4 ++-- .../network_settings_workflow_manager.py | 14 +++++++------- plugins/modules/template_workflow_manager.py | 6 ++++-- 5 files changed, 28 insertions(+), 21 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 2472f192c1..a890469c9d 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -302,14 +302,16 @@ def get_task_details(self, task_id): return result - def check_task_response_status(self, response, validation_string, data=False): + def check_task_response_status(self, response, validation_string, api_name="", data=False): """ Get the site id from the site name. Parameters: self - The current object details. response (dict) - API response. - validation_string (string) - String used to match the progress status. + validation_string (str) - String used to match the progress status. + api_name (str) - API name. + data (bool) - Set to True if the API is returning any information. Else, False. Returns: self @@ -336,8 +338,9 @@ def check_task_response_status(self, response, validation_string, data=False): while True: end_time = time.time() if (end_time - start_time) >= self.max_timeout: - self.log("Max timeout of {0} sec has reached for the execution id '{1}'. Exiting the loop due to unexpected API status." - .format(self.max_timeout, task_id), "WARNING") + self.log("Max timeout of {0} sec has reached for the execution id '{1}'. " + "Exiting the loop due to unexpected API '{2}' status." + .format(self.max_timeout, task_id, api_name), "WARNING") break task_details = self.get_task_details(task_id) @@ -388,12 +391,13 @@ def get_execution_details(self, execid): self.log("Response for the current execution: {0}".format(response)) return response - def check_execution_response_status(self, response): + def check_execution_response_status(self, response, api_name=""): """ Checks the reponse status provided by API in the Cisco Catalyst Center Parameters: response (dict) - API response + api_name (str) - API name Returns: self @@ -414,8 +418,9 @@ def check_execution_response_status(self, response): while True: end_time = time.time() if (end_time - start_time) >= self.max_timeout: - self.log("Max timeout of {0} sec has reached for the execution id '{1}'. Exiting the loop due to unexpected API status." - .format(self.max_timeout, execution_id), "WARNING") + self.log("Max timeout of {0} sec has reached for the execution id '{1}'. " + "Exiting the loop due to unexpected API '{2}' status." + .format(self.max_timeout, execution_id, api_name), "WARNING") break execution_details = self.get_execution_details(execution_id) diff --git a/plugins/modules/device_credential_workflow_manager.py b/plugins/modules/device_credential_workflow_manager.py index 48f7fbaba0..1d412f5013 100644 --- a/plugins/modules/device_credential_workflow_manager.py +++ b/plugins/modules/device_credential_workflow_manager.py @@ -2223,7 +2223,7 @@ def create_device_credentials(self): self.log("Received API response from 'create_global_credentials_v2': {0}" .format(response), "DEBUG") validation_string = "global credential addition performed" - self.check_task_response_status(response, validation_string).check_return_status() + self.check_task_response_status(response, validation_string, "create_global_credentials_v2").check_return_status() self.log("Global credential created successfully", "INFO") result_global_credential.update({ "Creation": { @@ -2288,7 +2288,7 @@ def update_device_credentials(self): self.log("Received API response for 'update_global_credentials_v2': {0}" .format(response), "DEBUG") validation_string = "global credential update performed" - self.check_task_response_status(response, validation_string).check_return_status() + self.check_task_response_status(response, validation_string, "update_global_credentials_v2").check_return_status() self.log("Updating device credential API input parameters: {0}" .format(final_response), "DEBUG") self.log("Global device credential updated successfully", "INFO") @@ -2344,7 +2344,7 @@ def assign_credentials_to_site(self): self.log("Received API response for 'assign_device_credential_to_site_v2': {0}" .format(response), "DEBUG") validation_string = "desired common settings operation successful" - self.check_task_response_status(response, validation_string).check_return_status() + self.check_task_response_status(response, validation_string, "assign_device_credential_to_site_v2").check_return_status() self.log("Device credential assigned to site {0} is successfully." .format(site_ids), "INFO") self.log("Desired State for assign credentials to a site: {0}" diff --git a/plugins/modules/ise_radius_integration_workflow_manager.py b/plugins/modules/ise_radius_integration_workflow_manager.py index d795d78fa1..d8d463f508 100644 --- a/plugins/modules/ise_radius_integration_workflow_manager.py +++ b/plugins/modules/ise_radius_integration_workflow_manager.py @@ -1338,7 +1338,7 @@ def update_auth_policy_server(self, ipAddress): params=auth_server_params, ) validation_string = "successfully updated aaa settings" - self.check_task_response_status(response, validation_string).check_return_status() + self.check_task_response_status(response, validation_string, "edit_authentication_and_policy_server_access_configuration").check_return_status() self.log("Authentication and Policy Server '{0}' updated successfully" .format(ipAddress), "INFO") result_auth_server.get("response").get(ipAddress) \ @@ -1399,7 +1399,7 @@ def delete_auth_policy_server(self, ipAddress): # Check the task status validation_string = "successfully deleted aaa settings" - self.check_task_response_status(response, validation_string).check_return_status() + self.check_task_response_status(response, validation_string, "delete_authentication_and_policy_server_access_configuration").check_return_status() taskid = response.get("response").get("taskId") # Update result information diff --git a/plugins/modules/network_settings_workflow_manager.py b/plugins/modules/network_settings_workflow_manager.py index b1f4e9d1a8..9cf3cc2a9e 100644 --- a/plugins/modules/network_settings_workflow_manager.py +++ b/plugins/modules/network_settings_workflow_manager.py @@ -2223,7 +2223,7 @@ def update_global_pool(self, global_pool): op_modifies=True, params=pool_params, ) - self.check_execution_response_status(response).check_return_status() + self.check_execution_response_status(response, "create_global_pool").check_return_status() self.log("Successfully created global pool successfully.", "INFO") for item in pool_params.get("settings").get("ippool"): name = item.get("ipPoolName") @@ -2267,7 +2267,7 @@ def update_global_pool(self, global_pool): params=pool_params, ) - self.check_execution_response_status(response).check_return_status() + self.check_execution_response_status(response, "update_global_pool").check_return_status() for item in pool_params.get("settings").get("ippool"): name = item.get("ipPoolName") self.log("Global pool '{0}' Updated successfully.".format(name), "INFO") @@ -2316,7 +2316,7 @@ def update_reserve_pool(self, reserve_pool): op_modifies=True, params=reserve_params, ) - self.check_execution_response_status(response).check_return_status() + self.check_execution_response_status(response, "reserve_ip_subpool").check_return_status() self.log("Successfully created IP subpool reservation '{0}'.".format(name), "INFO") result_reserve_pool.get("response") \ .update({name: self.want.get("wantReserve")[reserve_pool_index]}) @@ -2347,7 +2347,7 @@ def update_reserve_pool(self, reserve_pool): op_modifies=True, params=reserve_params, ) - self.check_execution_response_status(response).check_return_status() + self.check_execution_response_status(response, "update_reserve_ip_subpool").check_return_status() self.log("Reserved ip subpool '{0}' updated successfully.".format(name), "INFO") result_reserve_pool.get("response") \ .update({name: reserve_params}) @@ -2402,7 +2402,7 @@ def update_network(self, config): ) self.log("Received API response of 'update_network_v2': {0}".format(response), "DEBUG") validation_string = "desired common settings operation successful" - self.check_task_response_status(response, validation_string).check_return_status() + self.check_task_response_status(response, validation_string, "update_network_v2").check_return_status() self.log("Network has been changed successfully", "INFO") result_network.get("msg") \ .update({site_name: "Network Updated successfully"}) @@ -2469,7 +2469,7 @@ def delete_reserve_pool(self, reserve_pool_details): op_modifies=True, params={"id": _id}, ) - self.check_execution_response_status(response).check_return_status() + self.check_execution_response_status(response, "release_reserve_ip_subpool").check_return_status() executionid = response.get("executionId") result_reserve_pool = self.result.get("response")[1].get("reservePool") result_reserve_pool.get("response").update({name: {}}) @@ -2513,7 +2513,7 @@ def delete_global_pool(self, global_pool_details): ) # Check the execution status - self.check_execution_response_status(response).check_return_status() + self.check_execution_response_status(response, "delete_global_ip_pool").check_return_status() executionid = response.get("executionId") # Update result information diff --git a/plugins/modules/template_workflow_manager.py b/plugins/modules/template_workflow_manager.py index afa3fc2b17..976cbd6e5d 100644 --- a/plugins/modules/template_workflow_manager.py +++ b/plugins/modules/template_workflow_manager.py @@ -2181,6 +2181,7 @@ def handle_export(self, export): validation_string = "successfully exported project" self.check_task_response_status(response, validation_string, + "export_projects", True).check_return_status() self.result['response'][1].get("export").get("response").update({"exportProject": self.msg}) @@ -2200,6 +2201,7 @@ def handle_export(self, export): validation_string = "successfully exported template" self.check_task_response_status(response, validation_string, + "export_templates", True).check_return_status() self.result['response'][1].get("export").get("response").update({"exportTemplate": self.msg}) @@ -2248,7 +2250,7 @@ def handle_import(self, _import): params=_import_project, ) validation_string = "successfully imported project" - self.check_task_response_status(response, validation_string).check_return_status() + self.check_task_response_status(response, validation_string, "imports_the_projects_provided").check_return_status() self.result['response'][2].get("import").get("response").update({"importProject": validation_string}) else: self.msg = "Projects '{0}' already available.".format(payload) @@ -2334,7 +2336,7 @@ def handle_import(self, _import): params=import_template ) validation_string = "successfully imported template" - self.check_task_response_status(response, validation_string).check_return_status() + self.check_task_response_status(response, validation_string, "imports_the_templates_provided").check_return_status() self.result['response'][2].get("import").get("response") \ .update({"importTemplate": "Successfully imported the templates"}) From e6de4ad067d09686d36afcc335a7fb1e8f2c4d0f Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 7 Jun 2024 13:11:59 +0530 Subject: [PATCH 76/78] Addressed the review comments --- plugins/module_utils/dnac.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index a890469c9d..0dae9078f7 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -302,7 +302,7 @@ def get_task_details(self, task_id): return result - def check_task_response_status(self, response, validation_string, api_name="", data=False): + def check_task_response_status(self, response, validation_string, api_name, data=False): """ Get the site id from the site name. @@ -391,7 +391,7 @@ def get_execution_details(self, execid): self.log("Response for the current execution: {0}".format(response)) return response - def check_execution_response_status(self, response, api_name=""): + def check_execution_response_status(self, response, api_name): """ Checks the reponse status provided by API in the Cisco Catalyst Center From 677c548fa14bf8d73351bb35ff58e608bfdcb2c0 Mon Sep 17 00:00:00 2001 From: Madhan Date: Fri, 7 Jun 2024 14:24:35 +0530 Subject: [PATCH 77/78] Added changes in galaxy.yml and updated new version --- changelogs/changelog.yaml | 10 ++++++---- galaxy.yml | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 0047be7d7c..7f341b0352 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -916,8 +916,8 @@ releases: release_summary: Fix module name. minor_changes: - Fix module name from network_device_config__info to configuration_archive_details_info. - 6.15.1: - release_date: "2024-06-05" + 6.16.0: + release_date: "2024-06-07" changes: release_summary: Code changes in workflow manager modules. minor_changes: @@ -928,5 +928,7 @@ releases: - Added new attribute 'ise_integration_wait_time' in ise_radius_integration_workflow_manager.py - Added example playbooks in network_compliance_workflow_manager.py - Added detailed documentation in network_settings_workflow_manager.py - - Added new attribute 'provisioning' in provision_workflow_manager.py - - Added new attributes 'choices', 'failure_policy' in template_workflow_manager.py + - Added the code for creating/updating/deleting events subscription notification with specified destination and added the playbook and documentation with examples + - provision_workflow_manager.py - Added attribute 'provisioning' + - template_workflow_manager.py - Added attributes 'choices', 'failure_policy' + - events_and_notifications_workflow_manager.py - Added attributes 'webhook_event_notification', 'email_event_notification', 'syslog_event_notification' diff --git a/galaxy.yml b/galaxy.yml index 35a11ca248..d78490bb02 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,7 +1,7 @@ --- namespace: cisco name: dnac -version: 6.15.0 +version: 6.16.0 readme: README.md authors: - Rafael Campos From f41c0065bda60cad48e960bb5550168e5192d1fb Mon Sep 17 00:00:00 2001 From: Madhan Date: Fri, 7 Jun 2024 19:02:15 +0530 Subject: [PATCH 78/78] Added provision attribute --- plugins/modules/provision_workflow_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/provision_workflow_manager.py b/plugins/modules/provision_workflow_manager.py index fd72ec785f..f709c952b5 100644 --- a/plugins/modules/provision_workflow_manager.py +++ b/plugins/modules/provision_workflow_manager.py @@ -105,8 +105,8 @@ post /dna/intent/api/v1/business/sda/provision-device post /dna/intent/api/v1/wireless/provision - - Added 'provisioning' option in v6.15.1 - - Added provisioning and reprovisioning of wireless devices in v6.15.1 + - Added 'provisioning' option in v6.16.0 + - Added provisioning and reprovisioning of wireless devices in v6.16.0 """