From 7ab1cbfac16982af6a9779f2ec730e2cef2b07d2 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 23 Nov 2023 15:38:28 +0530 Subject: [PATCH 01/50] Add code for Update Device role, update interface details, create and assign Global User defined field to devices, Delete Global UDF, Delete Provisioned/Unprovisioned devices, convert playbook variable into snakecase, add examples and docstring to each API. --- playbooks/inventory_device.yml | 30 +- plugins/modules/inventory_intent.py | 969 +++++++++++++++++++++------- 2 files changed, 743 insertions(+), 256 deletions(-) diff --git a/playbooks/inventory_device.yml b/playbooks/inventory_device.yml index d489933506..81d93abba1 100644 --- a/playbooks/inventory_device.yml +++ b/playbooks/inventory_device.yml @@ -7,7 +7,7 @@ - "input_inventory.yml" - "credentials.yml" tasks: - - name: Add/Update/Resync/Delete the devices in DNAC Inventory. + - name: Add/Update/Resync/Delete the devices in Cisco DNA Center Inventory. cisco.dnac.inventory_intent: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" @@ -19,21 +19,21 @@ dnac_log: true state: merged config: - - enablePassword: "{{item.enablePassword}}" + - enable_password: "{{item.enable_password}}" password: "{{item.password}}" - ipAddress: "{{item.ipAddress}}" - snmpAuthPassphrase: "{{item.snmpAuthPassphrase}}" - snmpAuthProtocol: "{{item.snmpAuthProtocol}}" - snmpMode: "{{item.snmpMode}}" - snmpPrivPassphrase: "{{item.snmpPrivPassphrase}}" - snmpPrivProtocol: "{{item.snmpPrivProtocol}}" - snmpROCommunity: "{{item.snmpROCommunity}}" - snmpRWCommunity: "{{item.snmpRWCommunity}}" - snmpRetry: "{{item.snmpRetry}}" - snmpTimeout: "{{item.snmpTimeout}}" - snmpUserName: "{{item.snmpUserName}}" - userName: "{{item.userName}}" - device_resync: "{{item.resync}}" + ip_address: "{{item.ip_address}}" + snmp_auth_passphrase: "{{item.snmp_auth_passphrase}}" + snmp_auth_protocol: "{{item.snmp_auth_protocol}}" + snmp_mode: "{{item.snmp_mode}}" + snmp_priv_passphrase: "{{item.snmp_priv_passphrase}}" + snmp_priv_protocol: "{{item.snmp_priv_protocol}}" + snmp_ro_community: "{{item.snmp_ro_community}}" + snmp_rw_community: "{{item.snmp_rw_community}}" + snmp_username: "{{item.snmp_username}}" + username: "{{item.username}}" + device_updated: "{{item.device_updated}}" + clean_config: "{{item.clean_config}}" + type: "{{item.type}}" with_items: "{{ device_details }}" tags: diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 33a8e4498c..b340180675 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -35,109 +35,148 @@ elements: dict required: True suboptions: - cliTransport: - description: Network Device's cliTransport.Required for Adding Network Devices. + cli_transport: + description: Device's cli transport protocol. Required for Adding Network Devices. type: str - computeDevice: - description: ComputeDevice flag. + compute_device: + description: Compute Device flag. type: bool - enablePassword: - description: Network Device's enablePassword. + enable_password: + description: Device's enable password. type: str - extendedDiscoveryInfo: - description: Network Device's extendedDiscoveryInfo. + extended_discovery_info: + description: Device's extended discovery info. type: str - httpPassword: - description: Network Device's httpPassword.Required for Adding Compute, Meraki, Firepower Management Devices. + http_password: + description: Device's http password. Required for Adding Compute, Meraki, Firepower Management Devices. type: str - httpPort: - description: Network Device's httpPort.Required for Adding Compute, Firepower Management Devices. + http_port: + description: Device's http port number. Required for Adding Compute, Firepower Management Devices. type: str - httpSecure: + http_secure: description: HttpSecure flag. type: bool - httpUserName: - description: Network Device's httpUserName.Required for Adding Compute,Firepower Management Devices. + http_username: + description: Device's http username. Required for Adding Compute,Firepower Management Devices. type: str id: - description: Id path parameter. Device ID.Required for Deleting Device. + description: Id path parameter that is Device ID. Required for Deleting/Updating Device Roles. type: str - ipAddress: - description: Network Device's ipAddress.Required for Adding/Deleting/Resyncing Device except Meraki Devices. + ip_address: + description: Device's ipAddress. Required for Adding/Updating/Deleting/Resyncing Device except Meraki Devices. elements: str type: list - merakiOrgId: - description: Network Device's merakiOrgId. + meraki_org_id: + description: Device's meraki org id. elements: str type: list - netconfPort: - description: Network Device's netconfPort. + netconf_port: + description: Device's netconf port. type: str password: - description: Network Device's password.Required for Adding Network Device. + description: Device's password. Required for Adding Network Device. type: str - serialNumber: - description: Network Device's serialNumber. + serial_number: + description: Device's serial number. type: str - snmpAuthPassphrase: - description: Network Device's snmpAuthPassphrase.Required for Adding Network, Compute, Third Party Devices. + snmp_auth_passphrase: + description: Device's snmp auth passphrase. Required for Adding Network, Compute, Third Party Devices. type: str - snmpAuthProtocol: - description: Network Device's snmpAuthProtocol. + snmp_auth_protocol: + description: Device's snmp Auth Protocol. type: str default: "SHA" - snmpMode: - description: Network Device's snmpMode. + snmp_mode: + description: Device's snmp Mode. type: str default: "AUTHPRIV" - snmpPrivPassphrase: - description: Network Device's snmpPrivPassphrase.Required for Adding Network, Compute, Third Party Devices. + snmp_priv_passphrase: + description: Device's snmp Private Passphrase. Required for Adding Network, Compute, Third Party Devices. type: str - snmpPrivProtocol: - description: Network Device's snmpPrivProtocol.Required for Adding Network, Compute, Third Party Devices. + snmp_priv_protocol: + description: Device's snmp Private Protocol. Required for Adding Network, Compute, Third Party Devices. type: str default: "AES128" - snmpROCommunity: - description: Network Device's snmpROCommunity.Required for Adding V2C Devices. + snmp_ro_community: + description: Device's snmp ROCommunity. Required for Adding V2C Devices. type: str default: public - snmpRWCommunity: - description: Network Device's snmpRWCommunity.Required for Adding V2C Devices. + snmp_rw_community: + description: Device's snmp RWCommunity. Required for Adding V2C Devices. type: str default: private - snmpRetry: - description: Network Device's snmpRetry. + snmp_retry: + description: Device's snmp Retry. type: int default: 3 - snmpTimeout: - description: Network Device's snmpTimeout. + snmp_timeout: + description: Device's snmp Timeout. type: int default: 5 - snmpUserName: - description: Network Device's snmpUserName.Required for Adding Network, Compute, Third Party Devices. + snmp_username: + description: Device's snmp Username. Required for Adding Network, Compute, Third Party Devices. type: str - snmpVersion: - description: Network Device's snmpVersion. + snmp_version: + description: Device's snmp Version. type: str default: "v3" type: - description: Network Device's type. + description: Select Device's type from NETWORK_DEVICE, COMPUTE_DEVICE, MERAKI_DASHBOARD, THIRD_PARTY_DEVICE, FIREPOWER_MANAGEMENT_SYSTEM. type: str default: "NETWORK_DEVICE" - updateMgmtIPaddressList: - description: Network Device's updateMgmtIPaddressList. + update_mgmt_ipaddresslist: + description: Network Device's update Mgmt IPaddress List. + type: list elements: dict suboptions: - existMgmtIpAddress: - description: Network Device's existMgmtIpAddress. + exist_mgmt_ipaddress: + description: Device's existing Mgmt IpAddress. type: str - newMgmtIpAddress: - description: Network Device's newMgmtIpAddress. + new_mgmt_ipaddress: + description: Device's new Mgmt IpAddress. type: str - type: list - userName: - description: Network Device's userName.Required for Adding Network Device. + username: + description: Network Device's username. Required for Adding Network Device. + type: str + force_sync: + description: If forcesync is true then device sync would run in high priority thread if available, else the sync will fail. + type: bool + default: false + clean_config: + description: Required if need to delete the Provisioned device by clearing current configuration. + type: bool + default: false + role: + description: Role of device which can be ACCESS, CORE, DISTRIBUTION, BORDER ROUTER, UNKNOWN. + type: str + default: "ACCESS" + role_source: + description: role source for the Device. type: str + default: "AUTO" + name: + description: Name of Global User Defined Field. Required for creating/deleting UDF and then assigning it to device. + type: str + description: + description: Info about the global user defined field. Also used while updating interface details. + type: str + value: + description: Value to assign to tag with or without the same user defined field name. + type: str + admin_status: + description: Status of Interface of a device, it can be (UP/DOWN). + type: str + vlan_id: + description: Unique Id number assigned to a VLAN within a network. + type: int + voice_vlan_id: + description: Identifier used to distinguish a specific VLAN that is dedicated to voice traffic. + type: int + deployment_mode: + description: Preview/Deploy [Preview means the configuration is not pushed to the device. Deploy makes the configuration pushed to the device] + type: str + default: "Deploy" + requirements: - dnacentersdk >= 2.5.5 @@ -167,7 +206,7 @@ """ EXAMPLES = r""" -- name: Add new device in Inventory with full credentials +- name: Add/Update new device in Inventory with full credentials cisco.dnac.inventory_intent: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" @@ -179,38 +218,38 @@ dnac_log: False state: merged config: - - cliTransport: string - computeDevice: true - enablePassword: string - extendedDiscoveryInfo: string - httpPassword: string - httpPort: string - httpSecure: true - httpUserName: string - ipAddress: + - cli_transport: string + compute_device: true + enable_password: string + extended_discovery_info: string + http_password: string + http_port: string + http_secure: true + http_username: string + ip_address: - string - merakiOrgId: + meraki_org_id: - string - netconfPort: string + netconf_port: string password: string - serialNumber: string - snmpAuthPassphrase: string - snmpAuthProtocol: string - snmpMode: string - snmpPrivPassphrase: string - snmpPrivProtocol: string - snmpROCommunity: string - snmpRWCommunity: string - snmpRetry: 3 - snmpTimeout: 5 - snmpUserName: string - snmpVersion: string + serial_number: string + snmp_auth_passphrase: string + snmp_auth_protocol: string + snmp_mode: string + snmp_priv_passphrase: string + snmp_priv_protocol: string + snmp_ro_community: string + snmp_rw_community: string + snmp_retry: 3 + snmp_timeout: 5 + snmp_username: string + snmp_version: string type: string - updateMgmtIPaddressList: - - existMgmtIpAddress: string - newMgmtIpAddress: string - userName: string - deviceResync: false + update_mgmt_ipaddresslist: + - exist_mgmt_ipaddress: string + new_mgmt_ipaddress: string + username: string + device_resync: false - name: Add new Compute device in Inventory with full credentials.Inputs needed for Compute Device cisco.dnac.inventory_intent: @@ -224,20 +263,20 @@ dnac_log: False state: merged config: - - ipAddress: string - httpUserName: string - httpPassword: string - httpPort: string - snmpAuthPassphrase: string - snmpAuthProtocol: string - snmpMode: string - snmpPrivPassphrase: string - snmpPrivProtocol: string - snmpRetry: 3 - snmpTimeout: 5 - snmpUserName: string - userName: string - deviceResync: false + - ip_address: string + http_username: string + http_password: string + http_port: string + snmp_auth_passphrase: string + snmp_auth_protocol: string + snmp_mode: string + snmp_priv_passphrase: string + snmp_priv_protocol: string + snmp_retry: 3 + snmp_timeout: 5 + snmp_username: string + username: string + device_resync: false type: "COMPUTE_DEVICE" - name: Add new Meraki device in Inventory with full credentials.Inputs needed for Meraki Device. @@ -252,8 +291,8 @@ dnac_log: False state: merged config: - - httpPassword: string - deviceResync: false + - http_password: string + device_resync: false type: "MERAKI_DASHBOARD" - name: Add new Firepower Management device in Inventory with full credentials.Input needed to add Device. @@ -268,11 +307,11 @@ dnac_log: False state: merged config: - - ipAddress: string - httpUserName: string - httpPassword: string - httpPort: string - deviceResync: false + - ip_address: string + http_username: string + http_password: string + http_port: string + device_resync: false type: "FIREPOWER_MANAGEMENT_SYSTEM" - name: Add new Third Party device in Inventory with full credentials.Input needed to add Device. @@ -287,18 +326,77 @@ dnac_log: False state: merged config: - - ipAddress: string - snmpAuthPassphrase: string - snmpAuthProtocol: string - snmpMode: string - snmpPrivPassphrase: string - snmpPrivProtocol: string - snmpRetry: 3 - snmpTimeout: 5 - snmpUserName: string - deviceResync: false + - ip_address: string + snmp_auth_passphrase: string + snmp_auth_protocol: string + snmp_mode: string + snmp_priv_passphrase: string + snmp_priv_protocol: string + snmp_retry: 3 + snmp_timeout: 5 + snmp_username: string + device_resync: false type: "THIRD_PARTY_DEVICE" +- name: Update Device Role with IP Address + cisco.dnac.inventory_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: False + state: merged + config: + - ip_address: string + device_updated: true + update_device_role: + role: string + role_source: string + +- name: Update Interface details with IP Address + cisco.dnac.inventory_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: False + state: merged + config: + - ip_address: string + device_updated: true + update_interface_details: + - ip_address: "{{item.ip_address}}" + device_updated: "{{item.device_updated}}" + update_interface_details: + description: str + admin_status: str + vlan_id: int + voice_vlan_id: int + +- name: Create Global User Defined with IP Address + cisco.dnac.inventory_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: False + state: merged + config: + - ip_address: string + add_user_defined_field: + name: string + description: string + value: string + - name: Resync Device with IP Addresses cisco.dnac.inventory_intent: dnac_host: "{{dnac_host}}" @@ -311,9 +409,9 @@ dnac_log: False state: merged config: - - ipAddress: string - deviceResync: True - forceSync: False + - ip_address: string + device_resync: True + force_sync: False - name: Delete Device by id cisco.dnac.inventory_intent: @@ -327,9 +425,25 @@ dnac_log: False state: deleted config: - - cleanConfig: false + - clean_config: false id: string +- name: Delete Global User Defined Field with name + cisco.dnac.inventory_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: False + state: deleted + config: + - ip_address: string + add_user_defined_field: + name: string + """ RETURN = r""" @@ -383,34 +497,39 @@ def validate_input(self): 'self.msg' will describe the validation issues. """ - temp_spec = {'cliTransport': {'default': "telnet", 'type': 'str'}, - 'computeDevice': {'type': 'bool'}, - 'enablePassword': {'type': 'str'}, - 'extendedDiscoveryInfo': {'type': 'str'}, - 'httpPassword': {'type': 'str'}, - 'httpPort': {'type': 'str'}, - 'httpSecure': {'type': 'bool'}, - 'httpUserName': {'type': 'str'}, - 'ipAddress': {'type': 'list', 'elements': 'str'}, - 'merakiOrgId': {'type': 'list', 'elements': 'str'}, - 'netconfPort': {'type': 'str'}, + temp_spec = {'cli_transport': {'default': "telnet", 'type': 'str'}, + 'compute_device': {'type': 'bool'}, + 'enable_password': {'type': 'str'}, + 'extended_discovery_info': {'type': 'str'}, + 'http_password': {'type': 'str'}, + 'http_port': {'type': 'str'}, + 'http_secure': {'type': 'bool'}, + 'http_username': {'type': 'str'}, + 'ip_address': {'type': 'list', 'elements': 'str'}, + 'meraki_org_id': {'type': 'list', 'elements': 'str'}, + 'netconf_port': {'type': 'str'}, 'password': {'type': 'str'}, - 'serialNumber': {'type': 'str'}, - 'snmpAuthPassphrase': {'type': 'str'}, - 'snmpAuthProtocol': {'default': "SHA", 'type': 'str'}, - 'snmpMode': {'default': "AUTHPRIV", 'type': 'str'}, - 'snmpPrivPassphrase': {'type': 'str'}, - 'snmpPrivProtocol': {'default': "AES128", 'type': 'str'}, - 'snmpROCommunity': {'default': "public", 'type': 'str'}, - 'snmpRWCommunity': {'default': "private", 'type': 'str'}, - 'snmpRetry': {'default': 3, 'type': 'int'}, - 'snmpTimeout': {'default': 5, 'type': 'int'}, - 'snmpUserName': {'type': 'str'}, - 'snmpVersion': {'default': "v3", 'type': 'str'}, - 'updateMgmtIPaddressList': {'type': 'list', 'elements': 'dict'}, - 'userName': {'type': 'str'}, - 'deviceResync': {'type': 'bool'}, - 'forceSync': {'type': 'bool'} + 'serial_number': {'type': 'str'}, + 'snmp_auth_passphrase': {'type': 'str'}, + 'snmp_auth_protocol': {'default': "SHA", 'type': 'str'}, + 'snmp_mode': {'default': "AUTHPRIV", 'type': 'str'}, + 'snmp_priv_passphrase': {'type': 'str'}, + 'snmp_priv_protocol': {'default': "AES128", 'type': 'str'}, + 'snmp_ro_community': {'default': "public", 'type': 'str'}, + 'snmp_rw_community': {'default': "private", 'type': 'str'}, + 'snmp_retry': {'default': 3, 'type': 'int'}, + 'snmp_timeout': {'default': 5, 'type': 'int'}, + 'snmp_username': {'type': 'str'}, + 'snmp_version': {'default': "v3", 'type': 'str'}, + 'update_mgmt_ipaddresslist': {'type': 'list', 'elements': 'dict'}, + 'username': {'type': 'str'}, + 'update_device_role': {'type': 'dict'}, + 'device_resync': {'type': 'bool'}, + 'force_sync': {'type': 'bool'}, + 'clean_config': {'type': 'bool'}, + 'add_user_defined_field': {'type': 'dict'}, + 'upate_interface_details': {'type': 'dict'}, + 'deployment_mode': {'default': 'Deploy', 'type': 'str'} } # Validate device params @@ -432,20 +551,19 @@ def validate_input(self): return self - def device_exists_in_dnac(self, want_device): + def device_exists_in_dnac(self): """ Check which devices already exists in Cisco DNA Center and return both device_exist and device_not_exist in dnac. Args: - want_device (list): A list of devices you want to check for existence in Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Cisco DNA Center. Returns: list: A list of devices that exist in Cisco DNA Center. Description: - Queries Cisco DNA Center to check which devices from 'want_device' are already present. If a device is found - in Cisco DNA Center, its management IP address is included in the list of devices that exist. + Queries Cisco DNA Center to check which devices are already present in Cisco DNA Center and store + its management IP address in the list of devices that exist. Example: To use this method, create an instance of the class and call 'device_exists_in_dnac' on it, - passing a list of devices you want to check. The method returns a list of management IP addresses - for devices that exist in Cisco DNA Center. + The method returns a list of management IP addressesfor devices that exist in Cisco DNA Center. """ device_in_dnac = [] @@ -470,11 +588,95 @@ def device_exists_in_dnac(self, want_device): return device_in_dnac - def mandatory_parameter(self, config): + def is_udf_exist(self, field_name): + + udf_exists = False + response = self.dnac._exec( + family="devices", + function='get_all_user_defined_fields', + params={"name": field_name}, + ) + log(str(response)) + udf = response.get("response") + + if (len(udf) == 1): + udf_exists = True + else: + message = "Global User Defined Field with name - {0} doesnot exist in Cisco DNA Center".format(field_name) + self.log(message) + + return udf_exists + + def create_user_defined_field(self): + try: + payload = self.config[0].get('add_user_defined_field') + response = self.dnac._exec( + family="devices", + function='create_user_defined_field', + params=payload, + ) + self.log(str(response)) + response = response.get("response") + field_name = self.config[0].get('add_user_defined_field').get('name') + log("Global User Defined Field with name - {0} created successfully").format(field_name) + + except Exception as e: + error_message = "Error while Creating Global User Defined Field in Cisco DNA Center - {0}".format(str(e)) + log(error_message) + + return self + + def add_field_to_devices(self, device_ids): + field_details = self.config[0].get('add_user_defined_field') + field_name = field_details.get('name') + field_value = field_details.get('value', '1') + for device_id in device_ids: + payload = {} + payload['name'] = field_name + payload['value'] = field_value + udf_param_dict = { + 'payload': [payload], + 'device_id': device_id + } + try: + response = self.dnac._exec( + family="devices", + function='add_user_defined_field_to_device', + params=udf_param_dict, + ) + log(str(response)) + response = response.get("response") + + except Exception as e: + error_message = "Error while Adding Global UDF to device in Cisco DNA Center - {0}".format(str(e)) + log(error_message) + self.status = "failed" + self.result['changed'] = False + + return self + + def get_udf_id(self, field_name): + try: + response = self.dnac._exec( + family="devices", + function='get_all_user_defined_fields', + params={"name": field_name}, + ) + log(str(response)) + udf = response.get("response") + udf_id = udf[0].get("id") + + return udf_id + + except Exception as e: + error_message = "Cannot get the Id of Global UDF - from Cisco DNA Center - {0}".format(str(e)) + log(error_message) + + def mandatory_parameter(self): """ Check for and validate mandatory parameters for adding network devices in Cisco DNA Center. Args: - config (dict): A dictionary containing the configuration details for adding a network device to Cisco DNA Center. + self (object): An instance of a class used for interacting with Cisco Cisco DNA Center. Returns: dict: The input `config` dictionary if all mandatory parameters are present. Description: @@ -483,17 +685,18 @@ def mandatory_parameter(self, config): device_type = self.config[0].get("type", "NETWORK_DEVICE") params_dict = { - "NETWORK_DEVICE": ["enablePassword", "ipAddress", "password", "snmpUserName", "snmpAuthPassphrase", "snmpPrivPassphrase", "userName"], - "COMPUTE_DEVICE": ["ipAddress", "httpUserName", "httpPassword", "httpPort", "snmpUserName", "snmpAuthPassphrase", "snmpPrivPassphrase"], - "MERAKI_DASHBOARD": ["httpPassword"], - "FIREPOWER_MANAGEMENT_SYSTEM": ["ipAddress", "httpUserName", "httpPassword"], - "THIRD_PARTY_DEVICE": ["ipAddress", "snmpUserName", "snmpAuthPassphrase", "snmpPrivPassphrase"] + "NETWORK_DEVICE": ["enable_password", "ip_address", "password", "snmp_username", "snmp_auth_passphrase", "snmp_priv_passphrase", "username"], + "COMPUTE_DEVICE": ["ip_address", "http_username", "http_password", "http_port", "snmp_username", "snmp_auth_passphrase", "snmp_priv_passphrase"], + "MERAKI_DASHBOARD": ["http_password"], + "FIREPOWER_MANAGEMENT_SYSTEM": ["ip_address", "http_username", "http_password"], + "THIRD_PARTY_DEVICE": ["ip_address", "snmp_username", "snmp_auth_passphrase", "snmp_priv_passphrase"] } params_list = params_dict.get(device_type, []) + mandatory_params_absent = [] for param in params_list: - if param not in config: + if param not in self.config[0]: mandatory_params_absent.append(param) if mandatory_params_absent: @@ -501,8 +704,9 @@ def mandatory_parameter(self, config): self.result['msg'] = "Required parameters {0} for adding devices are not present".format(mandatory_params_absent) self.status = "failed" else: - self.result['msg'] = "Required paramter for Adding the devices in Inventory are present." - self.msg = "Required paramter for Adding the devices in Inventory are present." + log_msg = "Required paramter for Adding the devices in Inventory are present." + self.result['msg'] = log_msg + self.msg = log_msg self.status = "success" return self @@ -524,10 +728,10 @@ def get_have(self, config): """ have = {} - want_device = config.get("ipAddress") + want_device = config.get("ip_address") # Get the list of device that are present in Cisco DNA Center - device_in_dnac = self.device_exists_in_dnac(want_device) + device_in_dnac = self.device_exists_in_dnac() device_not_in_dnac = [] for ip in want_device: @@ -556,36 +760,47 @@ def get_device_params(self, params): """ device_param = { - "cli_transport": params.get("cliTransport"), - "enable_password": params.get("enablePassword"), + "cliTransport": params.get("cli_transport"), + "enablePassword": params.get("enable_password"), "password": params.get("password"), - "ipaddress": params.get("ipAddress"), - "snmp_auth_passphrase": params.get("snmpAuthPassphrase"), - "snmp_protocol": params.get("snmpAuthProtocol"), - "snmp_mode": params.get("snmpMode"), - "snmp_priv_passphrase": params.get("snmpPrivPassphrase"), - "snmp_priv_protocol": params.get("snmpPrivProtocol"), - "snmp_read_community": params.get("snmpROCommunity"), - "snmp_write_community": params.get("snmpRWCommunity"), - "snmp_retry": params.get("snmpRetry"), - "snmp_timeout": params.get("snmpTimeout"), - "snmp_username": params.get("snmpUserName"), - "username": params.get("userName"), - "compute_device": params.get("computeDevice"), - "extended_discovery_info": params.get("extendedDiscoveryInfo"), - "http_password": params.get("httpPassword"), - "http_port": params.get("httpPort"), - "http_secure": params.get("httpSecure"), - "http_username": params.get("httpUserName"), - "meraki_org_id": params.get("merakiOrgId"), - "netconf_port": params.get("netconfPort"), - "serial_number": params.get("serialNumber"), - "snmp_version": params.get("snmpVersion"), + "ipAddress": params.get("ip_address"), + "snmpAuthPassphrase": params.get("snmp_auth_passphrase"), + "snmpAuthProtocol": params.get("snmp_auth_protocol"), + "snmpMode": params.get("snmp_mode"), + "snmpPrivPassphrase": params.get("snmp_priv_passphrase"), + "snmpPrivProtocol": params.get("snmp_priv_protocol"), + "snmpROCommunity": params.get("snmp_ro_community"), + "snmpRWCommunity": params.get("snmp_rw_community"), + "snmpRetry": params.get("snmp_retry"), + "snmpTimeout": params.get("snmp_timeout"), + "snmpUserName": params.get("snmp_username"), + "userName": params.get("username"), + "computeDevice": params.get("compute_device"), + "extendedDiscoveryInfo": params.get("extended_discovery_info"), + "httpPassword": params.get("http_password"), + "httpPort": params.get("http_port"), + "httpSecure": params.get("http_secure"), + "httpUserName": params.get("http_username"), + "merakiOrgId": params.get("meraki_org_id"), + "netconfPort": params.get("netconf_port"), + "serialNumber": params.get("serial_number"), + "snmpVersion": params.get("snmp_version"), "type": params.get("type"), - "update_management_ip_list": params.get("updateMgmtIPaddressList"), - "force_sync": params.get("forceSync") + "updateMgmtIPaddressList": params.get("update_mgmt_ipaddresslist"), + "forceSync": params.get("force_sync"), + "cleanConfig": params.get("clean_config") } + if device_param.get("updateMgmtIPaddressList"): + temp_dict = device_param.get("updateMgmtIPaddressList")[0] + device_param["updateMgmtIPaddressList"][0] = {} + + device_param["updateMgmtIPaddressList"][0].update( + { + "existMgmtIpAddress": temp_dict.get("exist_mgmt_ipaddress"), + "newMgmtIpAddress": temp_dict.get("new_mgmt_ipaddress") + }) + return device_param def get_device_ids(self, device_ips): @@ -612,17 +827,40 @@ def get_device_ids(self, device_ips): if response: self.log(str(response)) - response = response.get("response")[0] - device_id = response["id"] + response = response.get("response") + if len(response) == 0: + continue + device_id = response[0]["id"] device_ids.append(device_id) except Exception as e: error_message = "Error while fetching device from Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) + log(error_message) raise Exception(error_message) return device_ids + def get_interface_from_ip(self, device_ip): + + try: + response = self.dnac._exec( + family="devices", + function='get_interface_by_ip', + params={"ip_address": device_ip} + ) + + if response: + self.log(str(response)) + response = response.get("response")[0] + interface_id = response["id"] + log("Fetch Interface Id for device {0} successfully !!".format(device_ip)) + return interface_id + + except Exception as e: + error_message = "Error while fetching Interface Id from Cisco DNA Center - {0}".format(str(e)) + log(error_message) + raise Exception(error_message) + def get_want(self, config): """ Get all the device related information from playbook that is needed to be @@ -661,20 +899,56 @@ def get_diff_merged(self, config): The updated results and status are stored in the class instance for further use. """ - device_added = False - device_updated = False - devices_to_add = self.have["device_not_in_dnac"] device_type = self.config[0].get("type", "NETWORK_DEVICE") - device_resynced = self.config[0].get("deviceResync", "False") + device_resynced = self.config[0].get("device_resync", False) + device_updated = self.config[0].get("device_updated", False) self.result['log'] = [] + if self.config[0].get('add_user_defined_field'): + field_name = self.config[0].get('add_user_defined_field').get('name') + if field_name is None: + self.msg = "Mandatory paramter for User Define Field - name is missing" + self.status = "failed" + return self + + # Check if the Global User defined field exist if not then create it with given field name + udf_exist = self.is_udf_exist(field_name) + if not udf_exist: + # Create the Global UDF + self.create_user_defined_field().check_return_status() + + # Get device Id with its IP Address + device_ips = config.get("ip_address") + device_ids = self.get_device_ids(device_ips) + if len(device_ids) == 0: + self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco DNA Center" + self.status = "failed" + self.result['changed'] = False + return self + + # Now add code for adding Global UDF to device with Id + self.add_field_to_devices(device_ids).check_return_status() + + self.result['changed'] = True + log_msg = "Global User Defined Added with name {0} added to device Successfully !".format(field_name) + log(log_msg) + self.result['log'].append(log_msg) + + return self + if device_resynced: # Code for triggers the resync operation using the retrieved device IDs and force sync parameter. - device_ips = config.get("ipAddress") + device_ips = config.get("ip_address") device_ids = self.get_device_ids(device_ips) + if len(device_ids) == 0: + self.msg = "Cannot perform the Resync operation as device's are not present in Cisco DNA Center" + self.status = "failed" + self.result['changed'] = False + return self + try: - force_sync = self.config[0].get("forceSync", "False") + force_sync = self.config[0].get("force_sync", "False") resync_param_dict = { 'payload': device_ids, 'force_sync': force_sync @@ -691,7 +965,6 @@ def get_diff_merged(self, config): task_id = response.get('response').get('taskId') while True: execution_details = self.get_task_details(task_id) - if 'Synced' in execution_details.get("progress"): self.status = "success" self.result['changed'] = True @@ -701,8 +974,7 @@ def get_diff_merged(self, config): self.msg = "Device Resynced get failed because of {0}".format(execution_details.get("failureReason")) self.status = "failed" break - self.log("Device Resynced Successfully") - self.log("Resynced devices are :" + str(device_ips)) + self.log("Device Resynced Successfully and Resynced devices are :" + str(device_ips)) msg = "Device " + str(device_ips) + " Resynced Successfully !!" self.result['log'].append(msg) @@ -713,35 +985,202 @@ def get_diff_merged(self, config): self.log(error_message) raise Exception(error_message) - if not devices_to_add: - # Write code for device updation - device_updated = True - - log("Devices {0} are present in Cisco DNA Center and updated successfully".format(config['ipAddress'])) - msg = "Devices {0} present in Cisco DNA Center and updated successfully".format(config['ipAddress']) - self.result['log'].append(msg) - self.status = "success" - return self - - # If we want to add device in inventory - self.mandatory_parameter(config).check_return_status() - config['ipAddress'] = devices_to_add config['type'] = device_type if device_type == "FIREPOWER_MANAGEMENT_SYSTEM": - config['httpPort'] = self.config[0].get("httpPort", "443") + config['http_port'] = self.config[0].get("http_port", "443") + + if device_updated: + device_to_update = self.config[0].get("ip_address") + # First check if device present in Cisco DNA Center or not + device_present = False + for device in device_to_update: + if device in self.have.get("device_in_dnac"): + device_present = True + break + + if not device_present: + msg = "Cannot perform Update operation as device - {0} not present in Cisco DNA Center".format(str(device_to_update)) + self.status = "success" + self.result['changed'] = False + self.result['response'] = msg + log(msg) + return self + + if self.config[0].get('update_device_role'): + for device_ip in device_to_update: + device_id = self.get_device_ids([device_ip]) + device_role_args = self.config[0].get('update_device_role') + if 'role' not in device_role_args or 'role_source' not in device_role_args: + self.msg = "Mandatory paramter(role/sourceRole) to update Device Role are missing" + self.status = "failed" + return self + + # Check if the same role of device is present in dnac then no need to change the state + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"managementIpAddress": device_ip} + ) + response = response.get('response')[0] + if response.get('role') == device_role_args.get('role'): + self.status = "success" + self.result['changed'] = False + log_msg = "Device Role - {0} same in Cisco DNA Center as well, no updation needed".format(device_role_args.get('role')) + self.result['log'] = log_msg + continue + + device_role_params = { + 'role': device_role_args.get('role'), + 'roleSource': device_role_args.get('role_source'), + 'id': device_id[0] + } - try: - response = self.dnac._exec( - family="devices", - function='add_device', - op_modifies=True, - params=config, - ) + try: + response = self.dnac._exec( + family="devices", + function='update_device_role', + op_modifies=True, + params=device_role_params, + ) + log(str(response)) + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + while True: + execution_details = self.get_task_details(task_id) + + if 'successfully' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + log("Device Role Updated Successfully") + msg = "Device " + str(device_to_update) + " Role updated Successfully !!" + self.result['log'].append(msg) + break + elif execution_details.get("isError") and execution_details.get("failureReason"): + self.msg = "Device Role Updation get failed because of {0}".format(execution_details.get("failureReason")) + self.status = "failed" + break + + except Exception as e: + error_message = "Error while Updating device role in Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) + raise Exception(error_message) + + return self + + if self.config[0].get('update_interface_details'): + # Call the Get interface details by device IP API and fetch the interface Id + for device_ip in device_to_update: + interface_id = self.get_interface_from_ip(device_ip) + # Now we call update interface details api with required parameter + try: + interface_params = self.config[0].get('update_interface_details') + temp_params = { + 'description': interface_params.get('description', ''), + 'adminStatus': interface_params.get('admin_status'), + 'voiceVlanId': interface_params.get('voice_vlan_id'), + 'vlanId': interface_params.get('vlan_id') + } + payload_params = {key: value for key, value in temp_params.items() if value is not None} + + update_interface_params = { + 'payload': payload_params, + 'interface_uuid': interface_id, + 'deployment_mode': interface_params.get('deployment_mode', 'Deploy') + } + response = self.dnac._exec( + family="devices", + function='update_interface_details', + op_modifies=True, + params=update_interface_params, + ) + log(str(response)) + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + while True: + execution_details = self.get_task_details(task_id) + + if 'SUCCESS' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + log_msg = "Update Interface Details for device {0} Added Successfully".format(device_ip) + log(log_msg) + self.result['log'].append(log_msg) + break + elif execution_details.get("isError") and execution_details.get("failureReason"): + self.msg = "Interface Updation get failed because of {0}".format(execution_details.get("failureReason")) + self.status = "failed" + break + + except Exception as e: + error_message = "Error while Updating Interface Details in Cisco DNA Center - {0}".format(str(e)) + self.status = "success" + self.result['changed'] = False + log_msg = "Port actions are only supported on user facing/access ports as it's not allowed or No Updation required" + self.result['log'] = log_msg + + return self + + else: + try: + self.mandatory_parameter().check_return_status() + response = self.dnac._exec( + family="devices", + function='sync_devices', + op_modifies=True, + params=self.want.get("device_params"), + ) + + log(str(response)) + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + while True: + execution_details = self.get_task_details(task_id) + + if execution_details.get("endTime"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + break + elif execution_details.get("isError") and execution_details.get("failureReason"): + self.msg = "Device Updation get failed because of {0}".format(execution_details.get("failureReason")) + self.status = "failed" + break + + log("Device Updated Successfully") + log("Updated devices are :" + str(device_to_update)) + msg = "Device " + str(device_to_update) + " updated Successfully !!" + self.result['log'].append(msg) + + except Exception as e: + error_message = "Error while Updating device in Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) + raise Exception(error_message) + + msg = "Devices {0} present in Cisco DNA Center and updated successfully".format(config['ip_address']) + self.log(msg) + self.result['log'].append(msg) + self.status = "success" - log(str(response)) - device_added = True + return self + + else: + # If we want to add device in inventory + config['ip_address'] = devices_to_add + self.mandatory_parameter().check_return_status() + try: + response = self.dnac._exec( + family="devices", + function='add_device', + op_modifies=True, + params=self.want.get("device_params"), + ) + log(str(response)) - if device_added or device_updated: if response and isinstance(response, dict): task_id = response.get('response').get('taskId') while True: @@ -749,23 +1188,26 @@ def get_diff_merged(self, config): if '/task/' in execution_details.get("progress"): self.status = "success" - self.result['changed'] = True self.result['response'] = execution_details + if len(devices_to_add) > 0: + self.result['changed'] = True + log("Device Added Successfully") + log("Added devices are :" + str(devices_to_add)) + msg = "Device " + str(devices_to_add) + " added Successfully !!" + self.result['log'].append(msg) + break + msg = "Devices " + str(self.config[0].get("ip_address")) + " already present in Cisco DNA Center" + self.result['log'].append(msg) break elif execution_details.get("isError") and execution_details.get("failureReason"): - self.msg = "Device Addition/Updation get failed because of {0}".format(execution_details.get("failureReason")) + self.msg = "Device Addition get failed because of {0}".format(execution_details.get("failureReason")) self.status = "failed" break - log("Device Added Successfully") - log("Added devices are :" + str(devices_to_add)) - msg = "Device " + str(devices_to_add) + " added Successfully !!" - self.result['log'].append(msg) - - except Exception as e: - error_message = "Error while Adding device in Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) - raise Exception(error_message) + except Exception as e: + error_message = "Error while Adding device in Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) + raise Exception(error_message) return self @@ -782,9 +1224,49 @@ def get_diff_deleted(self, config): raise Exception if any error occured. """ - device_to_delete = config.get("ipAddress") + device_to_delete = config.get("ip_address") self.result['msg'] = [] + if self.config[0].get('add_user_defined_field'): + field_name = self.config[0].get('add_user_defined_field').get('name') + udf_id = self.get_udf_id(field_name) + + if udf_id is None: + msg = "Global UDF - {0} is not present in Cisco DNA Center".format(field_name) + self.msg = msg + self.result['msg'] + self.status = "success" + self.result['changed'] = False + self.result['msg'].append(msg) + return self + + try: + response = self.dnac._exec( + family="devices", + function='delete_user_defined_field', + params={"id": udf_id}, + ) + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + while True: + execution_details = self.get_task_details(task_id) + if 'success' in execution_details.get("progress"): + self.msg = "Global UDF - {0} Deleted Successfully from Cisco DNA Center".format(field_name) + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + break + elif execution_details.get("isError") and execution_details.get("failureReason"): + self.msg = "Global UDF Deletion get failed because of {0}".format(execution_details.get("failureReason")) + self.status = "failed" + break + except Exception as e: + error_message = "Error while Deleting Global UDF from Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) + raise Exception(error_message) + + return self + for device_ip in device_to_delete: if device_ip not in self.have.get("device_in_dnac"): self.result['changed'] = False @@ -795,11 +1277,16 @@ def get_diff_deleted(self, config): continue device_id = self.get_device_ids([device_ip]) + delete_params = { + "id": device_id[0], + "clean_config": self.config[0].get("clean_config", False) + } + try: response = self.dnac._exec( family="devices", function='delete_device_by_id', - params={"id": device_id[0]}, + params=delete_params, ) if response and isinstance(response, dict): From 01b4411a2e53266653d9c80e5bbb1fd01c88c4b4 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 23 Nov 2023 15:47:33 +0530 Subject: [PATCH 02/50] fix ansible failed test case of adv for loop --- plugins/modules/inventory_intent.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index b340180675..3bf0d423b3 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -1082,7 +1082,10 @@ def get_diff_merged(self, config): 'voiceVlanId': interface_params.get('voice_vlan_id'), 'vlanId': interface_params.get('vlan_id') } - payload_params = {key: value for key, value in temp_params.items() if value is not None} + payload_params = {} + for key, value in temp_params.items(): + if value is not None: + payload_params[key] = value update_interface_params = { 'payload': payload_params, From 7149eb7749b5c6cb0acf788913859c015ef1831d Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 23 Nov 2023 19:31:13 +0530 Subject: [PATCH 03/50] addressed PR review comments on inventory module --- playbooks/inventory_device.yml | 4 +- plugins/modules/inventory_intent.py | 235 +++++++++++++++++----------- 2 files changed, 148 insertions(+), 91 deletions(-) diff --git a/playbooks/inventory_device.yml b/playbooks/inventory_device.yml index 81d93abba1..8a28c864a4 100644 --- a/playbooks/inventory_device.yml +++ b/playbooks/inventory_device.yml @@ -19,7 +19,8 @@ dnac_log: true state: merged config: - - enable_password: "{{item.enable_password}}" + - username: "{{item.username}}" + enable_password: "{{item.enable_password}}" password: "{{item.password}}" ip_address: "{{item.ip_address}}" snmp_auth_passphrase: "{{item.snmp_auth_passphrase}}" @@ -30,7 +31,6 @@ snmp_ro_community: "{{item.snmp_ro_community}}" snmp_rw_community: "{{item.snmp_rw_community}}" snmp_username: "{{item.snmp_username}}" - username: "{{item.username}}" device_updated: "{{item.device_updated}}" clean_config: "{{item.clean_config}}" type: "{{item.type}}" diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 3bf0d423b3..3cda5fc210 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -73,6 +73,9 @@ netconf_port: description: Device's netconf port. type: str + username: + description: Network Device's username. Required for Adding Network Device. + type: str password: description: Device's password. Required for Adding Network Device. type: str @@ -135,9 +138,6 @@ new_mgmt_ipaddress: description: Device's new Mgmt IpAddress. type: str - username: - description: Network Device's username. Required for Adding Network Device. - type: str force_sync: description: If forcesync is true then device sync would run in high priority thread if available, else the sync will fail. type: bool @@ -580,7 +580,7 @@ def device_exists_in_dnac(self): raise Exception(error_message) if response: - log(str(response)) + self.log(str(response)) response = response.get("response") for ip in response: device_ip = ip["managementIpAddress"] @@ -589,25 +589,46 @@ def device_exists_in_dnac(self): return device_in_dnac def is_udf_exist(self, field_name): + """ + Check if a Global User Defined Field exists in Cisco DNA Center based on its name. + Args: + self (object): An instance of a class used for interacting with Cisco DNA Center. + field_name (str): The name of the Global User Defined Field. + Returns: + bool: True if the Global User Defined Field exists, False otherwise. + Description: + The function sends a request to Cisco DNA Center to retrieve all Global User Defined Fields + with the specified name. If matching field is found, the function returns True, indicating that + the field exists else returns False. + """ - udf_exists = False response = self.dnac._exec( family="devices", function='get_all_user_defined_fields', params={"name": field_name}, ) - log(str(response)) + self.log(str(response)) udf = response.get("response") if (len(udf) == 1): - udf_exists = True - else: - message = "Global User Defined Field with name - {0} doesnot exist in Cisco DNA Center".format(field_name) - self.log(message) + return True + + message = "Global User Defined Field with name - {0} doesnot exist in Cisco DNA Center".format(field_name) + self.log(message) - return udf_exists + return False def create_user_defined_field(self): + """ + Create a Global User Defined Field in Cisco DNA Center based on the provided configuration. + Args: + self (object): An instance of a class used for interacting with Cisco DNA Center. + Returns: + self (object): An instance of a class used for interacting with Cisco DNA Center. + Description: + The function retrieves the configuration for adding a user-defined field from the configuration object, + sends the request to Cisco DNA Center to create the field, and logs the response. + """ try: payload = self.config[0].get('add_user_defined_field') response = self.dnac._exec( @@ -618,7 +639,8 @@ def create_user_defined_field(self): self.log(str(response)) response = response.get("response") field_name = self.config[0].get('add_user_defined_field').get('name') - log("Global User Defined Field with name - {0} created successfully").format(field_name) + self.log("Global User Defined Field with name - {0} created successfully").format(field_name) + self.status = "success" except Exception as e: error_message = "Error while Creating Global User Defined Field in Cisco DNA Center - {0}".format(str(e)) @@ -627,6 +649,18 @@ def create_user_defined_field(self): return self def add_field_to_devices(self, device_ids): + """ + Add a Global user-defined field with specified details to a list of devices in Cisco DNA Center. + Args: + self (object): An instance of a class used for interacting with Cisco DNA Center. + device_ids (list): A list of device IDs to which the user-defined field will be added. + Returns: + self (object): An instance of a class used for interacting with Cisco DNA Center. + Description: + The function retrieves the details of the user-defined field from the configuration object, + including the field name and default value then iterates over list of device IDs, creating a payload for + each device and sending the request to Cisco DNA Center to add the user-defined field. + """ field_details = self.config[0].get('add_user_defined_field') field_name = field_details.get('name') field_value = field_details.get('value', '1') @@ -644,8 +678,10 @@ def add_field_to_devices(self, device_ids): function='add_user_defined_field_to_device', params=udf_param_dict, ) - log(str(response)) + self.log(str(response)) response = response.get("response") + self.status = "success" + self.result['changed'] = True except Exception as e: error_message = "Error while Adding Global UDF to device in Cisco DNA Center - {0}".format(str(e)) @@ -656,22 +692,34 @@ def add_field_to_devices(self, device_ids): return self def get_udf_id(self, field_name): + """ + Get the ID of a Global User Defined Field in Cisco DNA Center based on its name. + Args: + self (object): An instance of a class used for interacting with Cisco Cisco DNA Center. + field_name (str): The name of the Global User Defined Field. + Returns: + str: The ID of the Global User Defined Field. + Description: + The function sends a request to Cisco DNA Center to retrieve all Global User Defined Fields + with the specified name and extracts the ID of the first matching field.If successful, it returns + the ID else returns None. + """ try: response = self.dnac._exec( family="devices", function='get_all_user_defined_fields', params={"name": field_name}, ) - log(str(response)) + self.log(str(response)) udf = response.get("response") udf_id = udf[0].get("id") - return udf_id - except Exception as e: error_message = "Cannot get the Id of Global UDF - from Cisco DNA Center - {0}".format(str(e)) log(error_message) + return udf_id + def mandatory_parameter(self): """ Check for and validate mandatory parameters for adding network devices in Cisco DNA Center. @@ -704,9 +752,7 @@ def mandatory_parameter(self): self.result['msg'] = "Required parameters {0} for adding devices are not present".format(mandatory_params_absent) self.status = "failed" else: - log_msg = "Required paramter for Adding the devices in Inventory are present." - self.result['msg'] = log_msg - self.msg = log_msg + self.msg = "Required parameter for Adding the devices in Inventory are present." self.status = "success" return self @@ -841,6 +887,18 @@ def get_device_ids(self, device_ips): return device_ids def get_interface_from_ip(self, device_ip): + """ + Get the interface ID for a device in Cisco DNA Center based on its IP address. + Args: + self (object): An instance of a class used for interacting with Cisco DNA Center. + device_ip (str): The IP address of the device. + Returns: + str: The interface ID for the specified device. + Description: + The function sends a request to Cisco DNA Center to retrieve the interface information + for the device with the provided IP address and extracts the interface ID from the + response, and returns the interface ID. + """ try: response = self.dnac._exec( @@ -848,12 +906,12 @@ def get_interface_from_ip(self, device_ip): function='get_interface_by_ip', params={"ip_address": device_ip} ) + self.log(str(response)) + response = response.get("response") - if response: - self.log(str(response)) - response = response.get("response")[0] - interface_id = response["id"] - log("Fetch Interface Id for device {0} successfully !!".format(device_ip)) + if len(response) > 0: + interface_id = response[0]["id"] + self.log("Fetch Interface Id for device {0} successfully !!".format(device_ip)) return interface_id except Exception as e: @@ -1003,7 +1061,7 @@ def get_diff_merged(self, config): self.status = "success" self.result['changed'] = False self.result['response'] = msg - log(msg) + self.log(msg) return self if self.config[0].get('update_device_role'): @@ -1042,7 +1100,7 @@ def get_diff_merged(self, config): op_modifies=True, params=device_role_params, ) - log(str(response)) + self.log(str(response)) if response and isinstance(response, dict): task_id = response.get('response').get('taskId') @@ -1098,7 +1156,7 @@ def get_diff_merged(self, config): op_modifies=True, params=update_interface_params, ) - log(str(response)) + self.log(str(response)) if response and isinstance(response, dict): task_id = response.get('response').get('taskId') @@ -1127,91 +1185,90 @@ def get_diff_merged(self, config): return self - else: - try: - self.mandatory_parameter().check_return_status() - response = self.dnac._exec( - family="devices", - function='sync_devices', - op_modifies=True, - params=self.want.get("device_params"), - ) - - log(str(response)) - - if response and isinstance(response, dict): - task_id = response.get('response').get('taskId') - while True: - execution_details = self.get_task_details(task_id) - - if execution_details.get("endTime"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - break - elif execution_details.get("isError") and execution_details.get("failureReason"): - self.msg = "Device Updation get failed because of {0}".format(execution_details.get("failureReason")) - self.status = "failed" - break - - log("Device Updated Successfully") - log("Updated devices are :" + str(device_to_update)) - msg = "Device " + str(device_to_update) + " updated Successfully !!" - self.result['log'].append(msg) - - except Exception as e: - error_message = "Error while Updating device in Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) - raise Exception(error_message) - - msg = "Devices {0} present in Cisco DNA Center and updated successfully".format(config['ip_address']) - self.log(msg) - self.result['log'].append(msg) - self.status = "success" - - return self - - else: - # If we want to add device in inventory - config['ip_address'] = devices_to_add - self.mandatory_parameter().check_return_status() + # Update Device details and credentails try: + self.mandatory_parameter().check_return_status() response = self.dnac._exec( family="devices", - function='add_device', + function='sync_devices', op_modifies=True, params=self.want.get("device_params"), ) - log(str(response)) + + self.log(str(response)) if response and isinstance(response, dict): task_id = response.get('response').get('taskId') while True: execution_details = self.get_task_details(task_id) - if '/task/' in execution_details.get("progress"): + if execution_details.get("endTime"): self.status = "success" + self.result['changed'] = True self.result['response'] = execution_details - if len(devices_to_add) > 0: - self.result['changed'] = True - log("Device Added Successfully") - log("Added devices are :" + str(devices_to_add)) - msg = "Device " + str(devices_to_add) + " added Successfully !!" - self.result['log'].append(msg) - break - msg = "Devices " + str(self.config[0].get("ip_address")) + " already present in Cisco DNA Center" - self.result['log'].append(msg) break elif execution_details.get("isError") and execution_details.get("failureReason"): - self.msg = "Device Addition get failed because of {0}".format(execution_details.get("failureReason")) + self.msg = "Device Updation get failed because of {0}".format(execution_details.get("failureReason")) self.status = "failed" break + log("Device Updated Successfully") + log("Updated devices are :" + str(device_to_update)) + msg = "Device " + str(device_to_update) + " updated Successfully !!" + self.result['log'].append(msg) + except Exception as e: - error_message = "Error while Adding device in Cisco DNA Center - {0}".format(str(e)) + error_message = "Error while Updating device in Cisco DNA Center - {0}".format(str(e)) self.log(error_message) raise Exception(error_message) + msg = "Devices {0} present in Cisco DNA Center and updated successfully".format(config['ip_address']) + self.log(msg) + self.result['log'].append(msg) + self.status = "success" + + return self + + # If we want to add device in inventory + config['ip_address'] = devices_to_add + self.mandatory_parameter().check_return_status() + try: + response = self.dnac._exec( + family="devices", + function='add_device', + op_modifies=True, + params=self.want.get("device_params"), + ) + self.log(str(response)) + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + while True: + execution_details = self.get_task_details(task_id) + + if '/task/' in execution_details.get("progress"): + self.status = "success" + self.result['response'] = execution_details + if len(devices_to_add) > 0: + self.result['changed'] = True + log("Device Added Successfully") + log("Added devices are :" + str(devices_to_add)) + msg = "Device " + str(devices_to_add) + " added Successfully !!" + self.result['log'].append(msg) + break + msg = "Devices " + str(self.config[0].get("ip_address")) + " already present in Cisco DNA Center" + self.result['log'].append(msg) + break + elif execution_details.get("isError") and execution_details.get("failureReason"): + self.msg = "Device Addition get failed because of {0}".format(execution_details.get("failureReason")) + self.status = "failed" + break + + except Exception as e: + error_message = "Error while Adding device in Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) + raise Exception(error_message) + return self def get_diff_deleted(self, config): From ec5e7c57a3b3d0cea995acd9b5044d845633ce68 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 24 Nov 2023 07:27:13 +0000 Subject: [PATCH 04/50] Adding bulk addition and bulk deletion, along with variable names changed --- playbooks/PnP.yml | 81 +++--- plugins/modules/pnp_intent.py | 499 +++++++++++++++++++++------------- 2 files changed, 356 insertions(+), 224 deletions(-) diff --git a/playbooks/PnP.yml b/playbooks/PnP.yml index be53564c03..0f3d864d57 100644 --- a/playbooks/PnP.yml +++ b/playbooks/PnP.yml @@ -19,19 +19,28 @@ tasks: - - name: Add a new device only + - name: Import devices in bulk cisco.dnac.pnp_intent: <<: *dnac_login dnac_log: True state: merged config: - - deviceInfo: - add_device_method: Single - serialNumber: FJC2330E0IK - hostname: Test-9300-6 - state: Unclaimed - pid: c9300-24P - isSudiRequired: True + - device_info: + - serial_number: FKC2310E0HB + hostname: 1-5 + state: Unclaimed + pid: c9300-24P + is_sudi_required: False + + - serial_number: FTC2320E0HB + hostname: 1-6 + state: Unclaimed + pid: c9300-24P + + - serial_number: ETC2320E0HB + hostname: 1-7 + state: Unclaimed + pid: c9300-24P - name: Add a new device and claim it cisco.dnac.pnp_intent: @@ -40,13 +49,12 @@ state: merged config: - site_name: Global/USA/San Francisco/BGL_18 - deviceInfo: - add_device_method: Single - serialNumber: FJC2330E0IK - hostname: Test-9300-6 - state: Unclaimed - pid: c9300-24P - isSudiRequired: True + device_info: + - serial_number: FJC2330E0BB + hostname: Test-9300-10 + state: Unclaimed + pid: c9300-24P + is_sudi_required: True - name: Claim an added Switch with template and image upgrade to a site only cisco.dnac.pnp_intent: @@ -58,11 +66,14 @@ template_name: "Ansible_PNP_Switch" image_name: cat9k_iosxe_npe.17.03.07.SPA.bin project_name: Onboarding Configuration - deviceInfo: - serialNumber: FJC271924EQ - hostname: Switch - state: Unclaimed - pid: C9300-48UXM + template_details: + hostname: SJC-Switch-1 + interface: TwoGigabitEthernet1/0/2 + device_info: + - serial_number: FJC271924EQ + hostname: Switch + state: Unclaimed + pid: C9300-48UXM - name: Claim an added Wireless Controller with template and image upgrade to a site only cisco.dnac.pnp_intent: @@ -71,25 +82,29 @@ state: merged config: - site_name: Global/USA/San Francisco/BGL_18 - pnp_type: CatalystWLC + pnp_type: catalyst_wlc template_name: "Ansible_PNP_WLC" image_name: C9800-40-universalk9_wlc.17.12.01.SPA.bin - deviceInfo: - serialNumber: FOX2639PAY7 - hostname: WLC - state: Unclaimed - pid: C9800-CL-K9 + template_params: + hostname: IAC-EWLC-Claimed + device_info: + - serial_number: FOX2639PAY7 + hostname: WLC + state: Unclaimed + pid: C9800-CL-K9 gateway: 204.192.101.1 - ipInterfaceName: TenGigabitEthernet0/0/0 - staticIP: 204.192.101.10 - subnetMask: 255.255.255.0 - vlanId: 1101 + ip_interface_name: TenGigabitEthernet0/0/0 + static_ip: 204.192.101.10 + subnet_mask: 255.255.255.0 + vlan_id: 1101 - - name: Delete an added device from the Pnp dashboard + - name: Delete multiple devices from the Pnp dashboard #If device is not present it won't fail cisco.dnac.pnp_intent: <<: *dnac_login dnac_log: True state: deleted config: - - deviceInfo: - serialNumber: FJC2330E0IK \ No newline at end of file + - device_info: + - serial_number: FKC2310E0HK + - serial_number: FTC2320E0HA + - serial_number: FKC2310E0HB diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 00f08cfd19..bb4e3ba6f3 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -38,6 +38,11 @@ template_name: description: Name of template to be configured on the device. type: str + template_params: + description: Parameter values for the parameterised templates. + Each varibale has a value that needs to be passed as key-value pair + in the dictionary. We can pass values as variable_name:variable_value. + type: dict image_name: description: Name of image to be configured on the device type: str @@ -47,36 +52,41 @@ site_name: description: Name of the site for which device will be claimed. type: str - projectName: + project_name: description: Name of the project under which the template is present type: str default: Onboarding Configuration pnp_type: - description: Device type of the Pnp device (Default/CatalystWLC/AccessPoint) + description: Device type of the Pnp device (Default/catalyst_wlc/access_point/stack_switch) type: str default: Default - staticIP: + static_ip: description: Management IP address of the Wireless Controller type: str - subnetMask: + subnet_mask: description: Subnet Mask of the Management IP address of the Wireless Controller type: str gateway: description: Gateway IP address of the Wireless Controller for getting pinged type: str - vlanId: + vlan_id: description: Vlan Id allocated for claimimg of Wireless Controller type: str - ipInterfaceName: + ip_interface_name: description: Name of the Interface used for Pnp by the Wireless Controller type: str - rfProfile: - description: rfprofile of the AP being claimed (HIGH/LOW/TYPICAL) + rf_profile: + description: Radio frequecy profile of the AP being claimed (HIGH/LOW/TYPICAL) type: str - deviceInfo: - description: Pnp Device's deviceInfo. - type: dict + device_info: + description: Pnp Device's device_info. This is mainly for adding the devices that are + not a part of the PnP database. For single addition the length of the list must be equal to one. + Followed by single addition a device can be claimed as well if site name is provided. + For Bulk Import of devices the size of the list must be greater than 1 and can be only used for adding. + For claiming the devices please use separate tasks or configs in the case of bulk import. + type: list required: true + elements: dict suboptions: hostname: description: Pnp Device's hostname. @@ -87,18 +97,15 @@ pid: description: Pnp Device's pid. type: str - serialNumber: - description: Pnp Device's serialNumber. + serial_number: + description: Pnp Device's serial_number. type: str - add_device_method: - description: Pnp Device's device addition method (Single/Bulk/Smart Account). - type: str - isSudiRequired: + is_sudi_required: description: Sudi Authentication requiremnet's flag. type: bool requirements: -- dnacentersdk == 2.6.5 +- dnacentersdk == 2.6.10 - python >= 3.5 notes: - SDK Method used are @@ -107,6 +114,7 @@ device_onboarding_pnp.DeviceOnboardingPnp.claim_a_device_to_a_site, device_onboarding_pnp.DeviceOnboardingPnp.delete_device_by_id_from_pnp, device_onboarding_pnp.DeviceOnboardingPnp.get_device_count, + device_onboarding_pnp.DeviceOnboardingPnp.get_device_by_id, sites.Sites.get_site, software_image_management_swim.SoftwareImageManagementSwim.get_software_image_details, configuration_templates.ConfigurationTemplates.gets_the_templates_available @@ -140,21 +148,21 @@ image_name: string golden_image: bool site_name: string - projectName: string + project_name: string pnp_type: string - staticIP: string - subnetMask: string + static_ip: string + subnet_mask: string gateway: string - vlanId: string - ipInterfaceName: string - rfProfile: string - deviceInfo: - hostname: string - state: string - pid: string - serialNumber: string - add_device_method: string - isSudiRequired: string + vlan_id: string + ip_interface_name: string + rf_profile: string + device_info: + - hostname: string + state: string + pid: string + serial_number: string + add_device_method: string + is_sudi_required: string """ RETURN = r""" @@ -236,19 +244,21 @@ def validate_input(self): pnp_spec = { 'template_name': {'type': 'str', 'required': False}, + 'template_params': {'type': 'dict', 'required': False}, 'project_name': {'type': 'str', 'required': False, 'default': 'Onboarding Configuration'}, 'site_name': {'type': 'str', 'required': False}, 'image_name': {'type': 'str', 'required': False}, 'golden_image': {'type': 'bool', 'required': False}, - 'deviceInfo': {'type': 'dict', 'required': True}, + 'device_info': {'type': 'list', 'required': True, + 'elements': 'dict'}, 'pnp_type': {'type': 'str', 'required': False, 'default': 'Default'}, - "rfProfile": {'type': 'str', 'required': False}, - "staticIP": {'type': 'str', 'required': False}, - "subnetMask": {'type': 'str', 'required': False}, + "rf_profile": {'type': 'str', 'required': False}, + "static_ip": {'type': 'str', 'required': False}, + "subnet_mask": {'type': 'str', 'required': False}, "gateway": {'type': 'str', 'required': False}, - "vlanId": {'type': 'str', 'required': False}, - "ipInterfaceName": {'type': 'str', 'required': False}, + "vlan_id": {'type': 'str', 'required': False}, + "ip_interface_name": {'type': 'str', 'required': False}, "sensorProfile": {'type': 'str', 'required': False} } @@ -256,7 +266,6 @@ def validate_input(self): valid_pnp, invalid_params = validate_list_of_dicts( self.config, pnp_spec ) - if invalid_params: self.msg = "Invalid parameters in playbook: {0}".format( "\n".join(invalid_params)) @@ -268,6 +277,7 @@ def validate_input(self): self.msg = "Successfully validated input" self.status = "success" + return self def get_site_details(self): @@ -355,11 +365,17 @@ def get_pnp_params(self, params): and stores it for further processing and calling the parameters in other APIs """ - - pnp_params = { - 'deviceInfo': params.get('deviceInfo') - } - return pnp_params + params_list = params["device_info"] + device_info_list = [] + for param in params_list: + device_dict = {} + param["serialNumber"] = param.pop("serial_number") + if "is_sudi_required" in param: + param["isSudiRequired"] = param.pop("is_sudi_required") + device_dict["deviceInfo"] = param + device_info_list.append(device_dict) + + return device_info_list def get_image_params(self, params): @@ -412,6 +428,17 @@ def get_claim_params(self): ] } + if configinfo["configId"] and self.validated_config[0]["template_params"]: + if isinstance(self.validated_config[0]["template_params"], dict): + if len(self.validated_config[0]["template_params"]) > 0: + configinfo["configParameters"] = [] + for key, value in self.validated_config[0]["template_params"].items(): + config_dict = { + 'key': key, + 'value': value + } + configinfo["configParameters"].append(config_dict) + claim_params = { 'deviceId': self.have.get('device_id'), 'siteId': self.have.get('site_id'), @@ -421,15 +448,17 @@ def get_claim_params(self): 'configInfo': configinfo, } - if claim_params["type"] == "CatalystWLC": - claim_params["staticIP"] = self.validated_config[0]['staticIP'] - claim_params["subnetMask"] = self.validated_config[0]['subnetMask'] + if claim_params["type"] == "catalyst_wlc": + claim_params["type"] = "CatalystWLC" + claim_params["staticIP"] = self.validated_config[0]['static_ip'] + claim_params["subnetMask"] = self.validated_config[0]['subnet_mask'] claim_params["gateway"] = self.validated_config[0]['gateway'] - claim_params["vlanId"] = str(self.validated_config[0]['vlanId']) - claim_params["ipInterfaceName"] = self.validated_config[0]['ipInterfaceName'] + claim_params["vlanId"] = str(self.validated_config[0]['vlan_id']) + claim_params["ipInterfaceName"] = self.validated_config[0]['ip_interface_name'] - if claim_params["type"] == "AccessPoint": - claim_params["rfProfile"] = self.validated_config[0]["rfProfile"] + if claim_params["type"] == "access_point": + claim_params["type"] = "AccessPoint" + claim_params["rfProfile"] = self.validated_config[0]["rf_profile"] return claim_params @@ -448,90 +477,110 @@ def get_have(self): Stored paramters are used to call the APIs to get the current image, template and site details to call the API for various types of devices """ - have = {} - if self.params.get("state") == "merged": - # check if given image exists, if exists store image_id - image_response = self.dnac_apply['exec']( - family="software_image_management_swim", - function='get_software_image_details', - params=self.want.get("image_params"), - ) - image_list = image_response.get("response") - self.log(str(image_response)) - - # check if project has templates or not - template_list = self.dnac_apply['exec']( - family="configuration_templates", - function='gets_the_templates_available', - params={"project_names": self.want.get("project_name")}, + + # Claiming is only allowed for single addition of devices + if len(self.want.get('pnp_params')) == 1: + # check if given device exists in pnp inventory, store device Id + device_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_list', + params={"serial_number": self.want.get("serial_number")} ) - self.log(str(template_list)) + self.log(str(device_response)) - # check if given site exits, if exists store current site info - site_exists = False - if not isinstance(self.want.get("site_name"), str) and \ - not self.want.get('pnp_params').get('deviceInfo').get('add_device_method'): - self.msg = "Name of the site must be a string" - self.status = "failed" + if not (device_response and (len(device_response) == 1)): + self.msg = "Adding the device to database" + self.status = "success" + self.have = have + have["device_found"] = False return self - site_name = self.want.get("site_name") - (site_exists, site_id) = self.get_site_details() - - if site_exists: - have["site_id"] = site_id - self.log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) - self.log("Site Name:" + str(site_name)) - if self.want.get("pnp_type") == "AccessPoint": - if self.get_site_type() != "floor": - self.msg = "Type of the site must \ - be a floor for claiming an AP" - self.status = "failed" - return self + have["device_found"] = True + have["device_id"] = device_response[0].get("id") + self.log("Device Id: " + str(have["device_id"])) - if len(image_list) == 1: - have["image_id"] = image_list[0].get("imageUuid") - self.log("Image Id: " + str(have["image_id"])) + if self.params.get("state") == "merged": + # check if given image exists, if exists store image_id + image_response = self.dnac_apply['exec']( + family="software_image_management_swim", + function='get_software_image_details', + params=self.want.get("image_params"), + ) + image_list = image_response.get("response") + self.log(str(image_response)) + + # check if project has templates or not + template_list = self.dnac_apply['exec']( + family="configuration_templates", + function='gets_the_templates_available', + params={"project_names": self.want.get("project_name")}, + ) + self.log(str(template_list)) - template_name = self.want.get("template_name") - if template_name: - if not (template_list and isinstance(template_list, list)): - self.msg = "Project Not Found \ - or Project is Empty" - self.status = "failed" - return self + dev_details_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="get_device_by_id", + params={"id": device_response[0].get("id")} + ) - template_details = get_dict_result(template_list, 'name', template_name) - if template_details: - have["template_id"] = template_details.get("templateId") - else: - self.msg = "Template Not found" - self.status = "failed" - return self + install_mode = dev_details_response.get("deviceInfo").get("mode") - else: - if not self.want.get('pnp_params').get('deviceInfo').get('add_device_method'): - self.msg = "Either Site Name or Device addition method must be provided" + # check if given site exits, if exists store current site info + site_exists = False + if not isinstance(self.want.get("site_name"), str) and \ + not self.want.get('pnp_params').get('deviceInfo'): + self.msg = "Name of the site must be a string" self.status = "failed" return self - # check if given device exists in pnp inventory, store device Id - device_response = self.dnac_apply['exec']( - family="device_onboarding_pnp", - function='get_device_list', - params={"serial_number": self.want.get("serial_number")} - ) - - self.log(str(device_response)) + site_name = self.want.get("site_name") + (site_exists, site_id) = self.get_site_details() + + if site_exists: + have["site_id"] = site_id + self.log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) + self.log("Site Name:" + str(site_name)) + if self.want.get("pnp_type") == "access_point": + if self.get_site_type() != "floor": + self.msg = "Type of the site must \ + be a floor for claiming an AP" + self.status = "failed" + return self + + if len(image_list) == 1: + if install_mode != "INSTALL": + self.msg = "Installation mode must be in \ + INSTALL mode to upgrade the image. Current mode is\ + {0}".format(install_mode) + self.status = "failed" + return self + + have["image_id"] = image_list[0].get("imageUuid") + self.log("Image Id: " + str(have["image_id"])) + + template_name = self.want.get("template_name") + if template_name: + if not (template_list and isinstance(template_list, list)): + self.msg = "Project Not Found \ + or Project is Empty" + self.status = "failed" + return self + + template_details = get_dict_result(template_list, 'name', template_name) + if template_details: + have["template_id"] = template_details.get("templateId") + else: + self.msg = "Template Not found" + self.status = "failed" + return self - if device_response and (len(device_response) == 1): - have["device_id"] = device_response[0].get("id") - have["device_found"] = True + else: + if not self.want.get('pnp_params').get('deviceInfo'): + self.msg = "Either Site Name or Device details must be added" + self.status = "failed" + return self - self.log("Device Id: " + str(have["device_id"])) - else: - have["device_found"] = False self.msg = "Successfully collected all project and template \ parameters from dnac for comparison" self.status = "success" @@ -563,29 +612,31 @@ def get_want(self, config): 'pnp_params': self.get_pnp_params(config), 'pnp_type': config.get('pnp_type'), 'site_name': config.get('site_name'), - 'serial_number': config.get('deviceInfo').get('serialNumber'), - 'hostname': config.get('deviceInfo').get('hostname'), 'project_name': config.get('project_name'), - 'template_name': config.get('template_name'), - 'add_device_method': config.get('deviceInfo').get('add_device_method'), - 'isSudiRequired': config.get('deviceInfo').get('isSudiRequired') + 'template_name': config.get('template_name') } + if len(self.want.get('pnp_params')) == 1: + self.want["serial_number"] = ( + self.want['pnp_params'][0]["deviceInfo"]. + get("serialNumber") + ) + self.want["hostname"] = ( + self.want['pnp_params'][0]["deviceInfo"]. + get("hostname") + ) - if self.want["pnp_type"] == "CatalystWLC": - self.want["staticIP"] = config.get('staticIP') - self.want["subnetMask"] = config.get('subnetMask') + if self.want["pnp_type"] == "catalyst_wlc": + self.want["static_ip"] = config.get('static_ip') + self.want["subnet_mask"] = config.get('subnet_mask') self.want["gateway"] = config.get('gateway') - self.want["vlanId"] = config.get('vlanId') - self.want["ipInterfaceName"] = config.get('ipInterfaceName') - - elif self.want["pnp_type"] == "AccessPoint": - if self.get_site_type() == "floor": - self.want["rfProfile"] = config.get("rfProfile") + self.want["vlan_id"] = config.get('vlan_id') + self.want["ip_interface_name"] = config.get('ip_interface_name') + elif self.want["pnp_type"] == "access_point": + self.want["rf_profile"] = config.get("rf_profile") self.msg = "Successfully collected all parameters from playbook " + \ "for comparison" self.status = "success" - return self def get_diff_merged(self): @@ -597,7 +648,8 @@ def get_diff_merged(self): self: An instance of a class used for interacting with Cisco DNA Center. Returns: object: An instance of the class with updated results and status - based on the processing of differences. + based on the processing of differences. Based on the length of devices passed + it adds/claims or does both. Description: The function processes the differences and, depending on the changes required, it may add, update,or resynchronize devices in @@ -605,50 +657,97 @@ def get_diff_merged(self): class instance for further use. """ - device_count_params = { + if not isinstance(self.want.get("pnp_params"), list): + self.msg = "Device Info must be passed as a list" + self.status = "failed" + return self + + if len(self.want.get("pnp_params")) >= 2: + devices_added = [] + for device in self.want.get("pnp_params"): + multi_device_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_list', + params={"serial_number": device["deviceInfo"]["serialNumber"]} + ) + + if (multi_device_response and (len(multi_device_response) == 1)): + devices_added.append(device) + if (len(self.want.get("pnp_params")) - len(devices_added)) == 0: + self.result['response'] = [] + self.result['msg'] = "Devices are already added" + return self + + bulk_list = [ + device + for device in self.want.get("pnp_params") + if device not in devices_added + ] + bulk_params = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="import_devices_in_bulk", + params={"payload": bulk_list}, + op_modifies=True, + ) + if len(bulk_params.get("successList")) > 0: + self.result['msg'] = "{0} device(s) imported successfully".format( + len(bulk_params.get("successList"))) + self.result['response'] = bulk_params + self.result['diff'] = self.validated_config + self.result['changed'] = True + return self + + self.msg = "Bulk import failed" + self.status = "failed" + return self + + provisioned_count_params = { "serial_number": self.want.get("serial_number"), "state": "Provisioned" } + planned_count_params = { + "serial_number": self.want.get("serial_number"), + "state": "Planned" + } + if not self.have.get("device_found"): - if not self.want["add_device_method"]: - self.msg = "Device needs to be added before claiming" + if not self.want['pnp_params']: + self.msg = "Device needs to be added before claiming. Please add device_info" self.status = "failed" return self if not self.want["site_name"]: - if self.want["add_device_method"] == "Single": - self.log("Adding device to pnp database") - dev_add_response = self.dnac_apply['exec']( - family="device_onboarding_pnp", - function="add_device", - params=self.want.get("pnp_params"), - op_modifies=True, - ) + self.log("Adding device to pnp database") + dev_add_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="add_device", + params=self.want.get('pnp_params')[0], + op_modifies=True, + ) - self.have["deviceInfo"] = dev_add_response.get("deviceInfo") - self.log(str(dev_add_response)) - if self.have["deviceInfo"]: - self.result['msg'] = "Only Device Added Successfully" - self.result['response'] = dev_add_response - self.result['diff'] = self.validated_config - self.result['changed'] = True - else: - self.msg = "Device Addition Failed" - self.status = "failed" - return self + self.have["deviceInfo"] = dev_add_response.get("deviceInfo") + self.log(str(dev_add_response)) + if self.have["deviceInfo"]: + self.result['msg'] = "Only Device Added Successfully" + self.result['response'] = dev_add_response + self.result['diff'] = self.validated_config + self.result['changed'] = True + else: + self.msg = "Device Addition Failed" + self.status = "failed" + return self else: - if self.want["add_device_method"] == "Single": - self.log("Adding device to pnp database") - dev_add_response = self.dnac_apply['exec']( - family="device_onboarding_pnp", - function="add_device", - params=self.want.get("pnp_params"), - op_modifies=True, - ) - self.have["deviceInfo"] = dev_add_response.get("deviceInfo") - self.log(str(dev_add_response)) + self.log("Adding device to pnp database") + dev_add_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="add_device", + params=self.want.get("pnp_params")[0], + op_modifies=True, + ) + self.have["deviceInfo"] = dev_add_response.get("deviceInfo") + self.log(str(dev_add_response)) claim_params = self.get_claim_params() claim_params["deviceId"] = dev_add_response.get("id") claim_response = self.dnac_apply['exec']( @@ -671,17 +770,27 @@ class instance for further use. return self else: - device_count_response = self.dnac_apply['exec']( + prov_dev_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_count', + op_modifies=True, + params=provisioned_count_params, + ) + plan_dev_response = self.dnac_apply['exec']( family="device_onboarding_pnp", function='get_device_count', op_modifies=True, - params=device_count_params, + params=planned_count_params, ) + if not self.want["site_name"]: self.result['response'] = self.have.get("device_found") self.result['msg'] = "Device is already added" else: - if device_count_response.get("response") == 0: + if ( + prov_dev_response.get("response") == 0 and + plan_dev_response.get("response") == 0 + ): claim_params = self.get_claim_params() self.log(str(claim_params)) claim_response = self.dnac_apply['exec']( @@ -700,7 +809,7 @@ class instance for further use. self.result['response'] = self.have.get("device_found") self.result['msg'] = "Device is already claimed" - return self + return self def get_diff_deleted(self): @@ -709,44 +818,52 @@ def get_diff_deleted(self): and is in unclaimed or failed state delete the given device Args: - self: An instance of a class used for interacting with Cisco DNA Center + self: An instance of a class used for interacting with Cisco DNA Center. + Here we pass a list of device info to be deleted Returns: self: An instance of the class with updated results and status based on - the deletion operation. + the deletion operation. It tells us the number of devices deleted if any of the devices + get deleted Description: This function is responsible for removing devices from the Cisco DNA Center PnP GUI and - raise Exception if any error occured. + pass nio changes if devices are already deleted. """ + devices_deleted = [] + devices_to_delete = self.want.get("pnp_params")[:] + for device in devices_to_delete: + multi_device_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_list', + params={"serial_number": device["deviceInfo"]["serialNumber"]} + ) + + if multi_device_response and len(multi_device_response) == 1: + device_id = multi_device_response[0].get("id") - if self.have.get("device_found"): - try: response = self.dnac_apply['exec']( family="device_onboarding_pnp", function="delete_device_by_id_from_pnp", op_modifies=True, - params={"id": self.have.get("device_id")}, + params={"id": device_id}, ) self.log(str(response)) - if response.get("deviceInfo").get("state") == "Deleted": - self.result['changed'] = True - self.result['response'] = response - self.result['diff'] = self.validated_config - self.result['msg'] = "Device Deleted Successfully" + if response.get("deviceInfo", {}).get("state") == "Deleted": + devices_deleted.append(device["deviceInfo"]["serialNumber"]) + self.want.get("pnp_params").remove(device) else: self.result['response'] = response self.result['msg'] = "Error while deleting the device" - except Exception as errorstr: - response = str(errorstr) - msg = "Device Deletion Failed" - self.module.fail_json(msg=msg, response=response) - + if len(devices_deleted) > 0: + self.result['changed'] = True + self.result['response'] = devices_deleted + self.result['diff'] = self.want.get("pnp_params") + self.result['msg'] = "{0} Device(s) Deleted Successfully".format(len(devices_deleted)) else: - self.msg = "Device Not Found" - self.status = "failed" - return self + self.result['msg'] = "Device(s) Not Found" + self.result['response'] = devices_deleted return self From a497c950db9231bae336d1158c27f71064e4ae5f Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 28 Nov 2023 14:23:34 +0530 Subject: [PATCH 05/50] Handle idempotenacy in delete operation and convert variable in snakecase convention --- playbooks/site_intent.yml | 32 +++++++ plugins/modules/site_intent.py | 156 ++++++++++++++++++++++++++++----- 2 files changed, 166 insertions(+), 22 deletions(-) create mode 100644 playbooks/site_intent.yml diff --git a/playbooks/site_intent.yml b/playbooks/site_intent.yml new file mode 100644 index 0000000000..9dfe260994 --- /dev/null +++ b/playbooks/site_intent.yml @@ -0,0 +1,32 @@ +- hosts: localhost + connection: local + gather_facts: no + vars_files: + - "credentials.yml" + tasks: + - name: Get site info and updating site details + cisco.dnac.site_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: true + state: merged + config: + - site: + floor: + name: Test_Floor2 + parent_name: 'Global/USA/San Francisco/BGL_18' + length: "101" + width: "75" + height: "50" + rf_model: 'Cubes And Walled Offices' + floor_number: 3 + - site: + area: + name: Canada + parent_name: 'Global' + type: area diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 37d491e7c3..3fccb11fff 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -70,7 +70,7 @@ name: description: Name of the building (eg building1). type: str - parentName: + parent_name: description: Parent name of building to be created. type: str floor: @@ -89,15 +89,15 @@ parentName: description: Complete Parent name of the floor to be created(eg Global/USA/San Francisco/BGL_18). type: str - rfModel: + rf_model: description: Type of floor. Allowed values are 'Cubes And Walled Offices', 'Drywall Office Only', 'Indoor High Ceiling', 'Outdoor Open Space'. type: str width: description: Width of the floor units is ft. (eg 100). type: int - floorNumber: - description: Floor number in the building/site (eg 5). + floor_number: + description: Floor number in the building/site (eg 5).once created, it can't be modified. type: int requirements: @@ -116,6 +116,24 @@ """ EXAMPLES = r""" +- name: Create a new area site + cisco.dnac.site_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: "{{dnac_log}}" + state: merged + config: + - site: + area: + name: string + parentName: string + type: string + - name: Create a new building site cisco.dnac.site_intent: dnac_host: "{{dnac_host}}" @@ -126,6 +144,7 @@ dnac_version: "{{dnac_version}}" dnac_debug: "{{dnac_debug}}" dnac_log: "{{dnac_log}}" + state: merged config: - site: building: @@ -135,6 +154,68 @@ name: string parentName: string type: string + +- name: Create a Floor site under the building + cisco.dnac.site_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: "{{dnac_log}}" + state: merged + config: + - site: + floor: + name: string + parentName: string + length: int + width: int + height: int + rfModel: string + floorNumber: int + type: string + +- name: Updating the Floor details under the building + cisco.dnac.site_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: "{{dnac_log}}" + state: merged + config: + - site: + floor: + name: string + parentName: string + length: int + width: int + height: int + type: string + +- name: Deleting any site you need site name and parentName + cisco.dnac.site_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: "{{dnac_log}}" + state: deleted + config: + - site: + floor: + name: string + parentName: string + type: string """ RETURN = r""" @@ -226,11 +307,11 @@ ) floor_plan = { - '57057': 'CUBES AND WALLED OFFICES', - '57058': 'DRYWELL OFFICE ONLY', - '41541500': 'FREE SPACE', - '57060': 'INDOOR HIGH CEILING', - '57059': 'OUTDOOR OPEN SPACE' + '101101': 'Cubes And Walled Offices', + '101102': 'Drywall Office Only', + '101105': 'Free Space', + '101104': 'Indoor High Ceiling', + '101103': 'Outdoor Open Space' } @@ -244,7 +325,6 @@ def __init__(self, module): def validate_input(self): """ Validate the fields provided in the playbook. - Checks the configuration provided in the playbook against a predefined specification to ensure it adheres to the expected structure and data types. Parameters: @@ -260,7 +340,7 @@ def validate_input(self): will contain the validated configuration. If it fails, 'self.status' will be 'failed', and 'self.msg' will describe the validation issues. """ - + if not self.config: self.msg = "config not available in playbook for validattion" self.status = "success" @@ -292,7 +372,6 @@ def validate_input(self): def get_current_site(self, site): """ Get the current site information. - Parameters: - self (object): An instance of the class containing the method. - site (list): A list containing information about the site. @@ -339,11 +418,11 @@ def get_current_site(self, site): floor=dict( name=site[0].get("name"), parentName=site[0].get("siteNameHierarchy").split("/" + site[0].get("name"))[0], - rfModel=floor_plan.get(rf_model), + rf_model=floor_plan.get(rf_model), width=map_geometry.get("attributes").get("width"), length=map_geometry.get("attributes").get("length"), height=map_geometry.get("attributes").get("height"), - floorNumber=map_geometry.get("attributes").get("floorNumber", "") + floorNumber=map_geometry.get("attributes").get("floor_number", "") ) ) @@ -417,16 +496,43 @@ def get_site_params(self, params): type. If the site type is 'floor', it ensures that the 'rfModel' parameter is stored in uppercase. """ - - site = params.get("site") typeinfo = params.get("type") + site_info = {} - if typeinfo == "floor": - site["floor"]["rfModel"] = site.get("floor").get("rfModel").upper() + if typeinfo == 'area': + area_details = params.get('site').get('area') + site_info['area'] = { + 'name': area_details.get('name'), + 'parentName': area_details.get('parent_name') + } + elif typeinfo == 'building': + building_details = params.get('site').get('building') + site_info['building'] = { + 'name': building_details.get('name'), + 'address': building_details.get('address', ""), + 'parentName': building_details.get('parent_name'), + 'latitude': building_details.get('latitude'), + 'longitude': building_details.get('longitude'), + 'country': building_details.get('country') + } + else: + floor_details = params.get('site').get('floor') + site_info['floor'] = { + 'name': floor_details.get('name'), + 'parentName': floor_details.get('parent_name'), + 'length': floor_details.get('length'), + 'width': floor_details.get('width'), + 'height': floor_details.get('height'), + 'floorNumber': floor_details.get('floor_number', '') + } + try: + site_info["floor"]["rfModel"] = floor_details.get("rf_model") + except Exception as e: + log("Floor doesnot have rfModel attribute") site_params = dict( type=typeinfo, - site=site, + site=site_info, ) return site_params @@ -446,10 +552,9 @@ def get_site_name(self, site): """ site_type = site.get("type") - parent_name = site.get("site").get(site_type).get("parentName") + parent_name = site.get("site").get(site_type).get("parent_name") name = site.get("site").get(site_type).get("name") site_name = '/'.join([parent_name, name]) - log(site_name) return site_name @@ -579,6 +684,7 @@ def get_diff_merged(self, config): # Existing Site requires update site_params = self.want.get("site_params") site_params["site_id"] = self.have.get("site_id") + response = self.dnac._exec( family="sites", function='update_site', @@ -595,6 +701,7 @@ def get_diff_merged(self, config): else: # Creating New Site + site_params = self.want.get("site_params") response = self.dnac._exec( family="sites", function='create_site', @@ -690,7 +797,12 @@ def get_diff_deleted(self, config): break else: - self.module.fail_json(msg="Site Not Found", response=[]) + msg = "Cannot delete Site - {0} as it's not found in Cisco DNA Center".format(self.want.get("site_name")) + self.status = "success" + self.result['changed'] = False + self.result['response'] = msg + log(msg) + self.result['msg'] = msg return self From 54b26c59f568856b53bdf07aa4c27814847e88c5 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 28 Nov 2023 14:25:22 +0530 Subject: [PATCH 06/50] revert floor rfModel number --- plugins/modules/site_intent.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 3fccb11fff..44ad223667 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -307,11 +307,11 @@ ) floor_plan = { - '101101': 'Cubes And Walled Offices', - '101102': 'Drywall Office Only', - '101105': 'Free Space', - '101104': 'Indoor High Ceiling', - '101103': 'Outdoor Open Space' + '57057': 'CUBES AND WALLED OFFICES', + '57058': 'DRYWELL OFFICE ONLY', + '41541500': 'FREE SPACE', + '57060': 'INDOOR HIGH CEILING', + '57059': 'OUTDOOR OPEN SPACE' } From 8898dca2d3e605eb67de09fca395292ef0564e96 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 29 Nov 2023 10:28:15 +0530 Subject: [PATCH 07/50] changed variable name in the documentation --- plugins/modules/site_intent.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 44ad223667..bd4e1485fe 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -170,12 +170,12 @@ - site: floor: name: string - parentName: string + parent_name: string length: int width: int height: int - rfModel: string - floorNumber: int + rf_model: string + floor_number: int type: string - name: Updating the Floor details under the building @@ -193,7 +193,7 @@ - site: floor: name: string - parentName: string + parent_name: string length: int width: int height: int @@ -214,7 +214,7 @@ - site: floor: name: string - parentName: string + parent_name: string type: string """ From 0472525ea04e277eb742aeb8536857c340a4a6d9 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 30 Nov 2023 10:13:14 +0530 Subject: [PATCH 08/50] changes log to self.log --- plugins/modules/site_intent.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index bd4e1485fe..a9845f9d21 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -466,10 +466,10 @@ def site_exists(self): ) except Exception as e: - log("The input site {0} is not valid or site is not present.".format(self.want.get("site_name"))) + self.log("The input site {0} is not valid or site is not present.".format(self.want.get("site_name"))) if response: - log(str(response)) + self.log(str(response)) response = response.get("response") current_site = self.get_current_site(response) @@ -528,7 +528,7 @@ def get_site_params(self, params): try: site_info["floor"]["rfModel"] = floor_details.get("rf_model") except Exception as e: - log("Floor doesnot have rfModel attribute") + self.log("Floor doesnot have rfModel attribute") site_params = dict( type=typeinfo, @@ -555,7 +555,7 @@ def get_site_name(self, site): parent_name = site.get("site").get(site_type).get("parent_name") name = site.get("site").get(site_type).get("name") site_name = '/'.join([parent_name, name]) - log(site_name) + self.log(site_name) return site_name @@ -577,8 +577,8 @@ def site_requires_update(self): requested_site = self.want.get("site_params") current_site = self.have.get("current_site") - log("Current Site: " + str(current_site)) - log("Requested Site: " + str(requested_site)) + self.log("Current Site: " + str(current_site)) + self.log("Requested Site: " + str(requested_site)) obj_params = [ ("type", "type"), @@ -729,7 +729,7 @@ def get_diff_merged(self, config): if site_updated: log_msg = "Site - {0} Updated Successfully".format(self.want.get("site_name")) - log(log_msg) + self.log(log_msg) self.result['msg'] = log_msg self.result['response'].update({"siteId": self.have.get("site_id")}) @@ -739,8 +739,8 @@ def get_diff_merged(self, config): if site_exists: log_msg = "Site - {0} Created Successfully".format(current_site) - log(log_msg) - log("Current site:" + str(current_site)) + self.log(log_msg) + self.log("Current site:" + str(current_site)) self.result['msg'] = log_msg self.result['response'].update({"siteId": current_site.get('site_id')}) @@ -801,7 +801,7 @@ def get_diff_deleted(self, config): self.status = "success" self.result['changed'] = False self.result['response'] = msg - log(msg) + self.log(msg) self.result['msg'] = msg return self From 1c2d5c49fc43fb6d30f503fd1e5a4c8f3ed76b0e Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Thu, 30 Nov 2023 14:02:08 +0000 Subject: [PATCH 09/50] Adding the discovery intent module --- playbooks/discovery_intent.yml | 50 +++ plugins/modules/discovery_intent.py | 641 ++++++++++++++++++++++++++++ 2 files changed, 691 insertions(+) create mode 100644 playbooks/discovery_intent.yml create mode 100644 plugins/modules/discovery_intent.py diff --git a/playbooks/discovery_intent.yml b/playbooks/discovery_intent.yml new file mode 100644 index 0000000000..2db0a5bc46 --- /dev/null +++ b/playbooks/discovery_intent.yml @@ -0,0 +1,50 @@ +--- +- name: Discover devices + hosts: localhost + connection: local + gather_facts: no + + vars_files: + - "{{ CLUSTERFILE }}" + + vars: + dnac_login: &dnac_login + 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 }}" + + tasks: + - name: Execute discovery devices + cisco.dnac.discovery_intent: + <<: *dnac_login + state: merged + config: + - devices_list: + - name: SJ-BN-9300 + site: Global/USA/SAN JOSE/BLD23 + role: MAPSERVER,BORDERNODE,INTERNAL,EXTERNAL,SDATRANSIT + l2interface: TenGigabitEthernet1/1/8 + ip: 204.1.2.1 + - name: NY-BN-9300 + site: Global/USA/New York/BLDNYC + role: MAPSERVER,BORDERNODE,INTERNAL,EXTERNAL,SDATRANSIT,NOIPTRANSIT,ECA + managed_ap_site: Global/USA/New York/BLDNYC/FLOOR1 + rolling_ap_count: 25 + l2interface: TenGigabitEthernet1/1/6 + ip: 204.1.2.3 + discovery_type: "MULTI RANGE" + protocol_order: ssh + start_index: 1 + records_to_return: 25 + snmp_version: v2 + + register: discovery_task + + - name: Debug get discovery task + ansible.builtin.debug: + msg: "{{discovery_task}}" + diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py new file mode 100644 index 0000000000..4834e4124f --- /dev/null +++ b/plugins/modules/discovery_intent.py @@ -0,0 +1,641 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Cisco Systems +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = ("Abinash Mishra, Phan Nguyen") + +DOCUMENTATION = r""" +--- +module: discovery_intent +short_description: Resource module for discovery related functions +description: +- Manage operations discover devices using IP address/range, CDP, LLDP and delete discoveries +- API to discover a device or multiple devices +- API to delete a device or multiple devices +version_added: '6.6.0' +extends_documentation_fragment: + - cisco.dnac.intent_params +author: Abinash Mishra (@abimishr) + Phan Nguyen (phannguy) +options: + state: + description: The state of DNAC after module completion. + type: str + choices: [ merged, deleted ] + default: merged + config: + description: + - List of details of device being managed. + type: list + elements: dict + required: true + suboptions: + devices_list: + description: List of devices with details necessary for discovering the devices. + type: list + elements: dict + required: true + suboptions: + name: + description: Hostname of the device + type: str + ip: + description: Management IP address of the device + type: str + required: true + discovery_type: + description: Type of discovery (SINGLE/RANGE/MULTI RANGE/CDP/LLDP/CIDR) + type: str + required: true + cdp_level: + description: Total number of levels that are there in cdp's method of discovery + type: int + default: 16 + lldp_level: + description: Total number of levels that are there in lldp's method of discovery + type: int + default: 16 + start_index: + description: Start index for the header in fetching global v2 credentials + type: int + enable_password_list: + description: List of enable passwords for the CLI crfedentials + type: list + elements: str + records_to_return: + description: Number of records to returnfor the header in fetching global v2 credentials + type: int + http_read_credential: + description: HTTP read credentials for hosting a device + type: dict + http_write_credential: + description: HTTP write credentials for hosting a device + type: dict + ip_filter_list: + description: List of IP adddrsess that needs to get filtered out from the IP addresses added + type: list + elements: str + discovery_name: + description: Name of the discovery task + type: dict + netconf_port: + description: Port for the netconf credentials + type: str + password_list: + description: List of passwords for the CLI credentials + type: list + elements: str + username_list: + description: List of passwords for the CLI credentials + type: list + elements: str + preferred_mgmt_ip_method: + description: Preferred method for the management of the IP (None/UseLoopBack) + type: str + default: None + protocol_order: + description: Order of protocol (ssh/telnet) in which device connection will be tried. For example, 'telnet' - only telnet - 'ssh, + telnet' - ssh with higher order than telnet + type: str + retry: + description: Number of times to try establishing connection to device + type: int + snmp_auth_passphrase: + description: Auth Pass phrase for SNMP + type: str + snmp_auth_protocol: + description: SNMP auth protocol (SHA/MD5) + type: str + snmp_mode: + description: Mode of SNMP (AUTHPRIV/AUTHNOPRIV/NOAUTHNOPRIV) + type: str + snmp_priv_passphrase: + description: Pass phrase for SNMP privacy + type: str + snmp_priv_protocol: + description: SNMP privacy protocol (DES/AES128) + type: str + snmp_ro_community: + description: Snmp RO community of the devices to be discovered + type: str + snmp_ro_community_desc: + description: Description for Snmp RO community + type: str + snmp_rw_community: + description: Snmp RW community of the devices to be discovered + type: str + snmp_rw_community_desc: + description: Description for Snmp RW community + type: str + snmp_username: + description: SNMP username of the device + type: str + snmp_version: + description: Version of SNMP (v2/v3) + type: str + required: true + timeout: + description: Time to wait for device response in seconds + type: int +requirements: +- dnacentersdk == 2.6.10 +- python >= 3.5 +notes: + - SDK Method used are + discovery.Discovery.get_all_global_credentials_v2, + discovery.Discovery.start_discovery, + task.Task.get_task_by_id, + discovery.Discovery.get_discoveries_by_range, + discovery.Discovery.get_discovered_network_devices_by_discovery_id', + discovery.Discovery.delete_discovery_by_id + + - Paths used are + get /dna/intent/api/v2/global-credential + post /dna/intent/api/v1/discovery + get /dna/intent/api/v1/task/{taskId} + get /dna/intent/api/v1/discovery/{startIndex}/{recordsToReturn} + get /dna/intent/api/v1/discovery/{id}/network-device + delete /dna/intent/api/v1/discovery/{id} + +""" + +EXAMPLES = r""" +- name: Execute discovery devices + cisco.dnac.discovery_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + state: merged + config: + - device_list: + - name: string + ip: string + discovery_type: string + cdp_level: string + lldp_level: string + start_index: integer + enable_pasword_list: list + records_to_return: integer + http_read_credential: string + http_write_credential: string + ip_filter_list: list + discovery_name: string + password_list: list + preffered_mgmt_ip_method: string + protocol_order: string + retry: integer + snmp_auth_passphrase: string + snmp_auth_protocol: string + snmp_mode: string + snmp_priv_passphrse: string + snmp_priv_protocol: string + snmp_ro_community: string + snmp_ro_community_desc: string + snmp_rw_community: string + snmp_rw_community_desc: string + snmp_username: string + snmp_version: string + timeout: integer + username_list: list +""" + +RETURN = r""" +#Case_1: When the device(s) are discovered successfully. +response_1: + description: A dictionary with the response returned by the Cisco DNAC Python SDK + returned: always + type: dict + sample: > + { + "response": + { + "response": String, + "version": String + }, + "msg": String + } + +#Case_2: Given device details or SNMP mode are not provided +response_2: + description: A list with the response returned by the Cisco DNAC Python SDK + returned: always + type: list + sample: > + { + "response": [], + "msg": String + } + +#Case_3: Error while deleting a discovery +response_3: + description: A string with the response returned by the Cisco DNAC Python SDK + returned: always + type: dict + sample: > + { + "response": String, + "msg": String + } +""" +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( + DnacBase, + validate_list_of_dicts +) +import time +import re + + +class DnacDiscovery(DnacBase): + def __init__(self, module): + super().__init__(module) + self.creds_ids_list = [] + + def validate_input(self): + """ + Validate the fields provided in the playbook. Checks the + configuration provided in the playbook against a predefined + specification to ensure it adheres to the expected structure + and data types. + + 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: + - self.msg: A message describing the validation result. + - self.status: The status of the validation (either 'success' or 'failed'). + - self.validated_config: If successful, a validated version of the + 'config' parameter. + Example: + To use this method, create an instance of the class and call + 'validate_input' on it.If the validation succeeds, 'self.status' + will be 'success'and 'self.validated_config' will contain the + validated configuration. If it fails, 'self.status' will be + 'failed', and 'self.msg' will describe the validation issues. + """ + + if not self.config: + self.msg = "config not available in playbook for validation" + self.status = "success" + return self + + discovery_spec = { + 'cdp_level': {'type': 'int', 'required': False, + 'default': 16}, + 'discovery_type': {'type': 'str', 'required': True}, + 'enable_password_list': {'type': 'list', 'required': False, + 'elements': 'str'}, + 'devices_list': {'type': 'list', 'required': True, + 'elements': 'dict'}, + 'start_index': {'type': 'int', 'required': False}, + 'records_to_return': {'type': 'int', 'required': False}, + 'http_read_credential': {'type': 'dict', 'required': False}, + 'http_write_credential': {'type': 'dict', 'required': False}, + 'ip_filter_list': {'type': 'list', 'required': False, + 'elements': 'str'}, + 'lldp_level': {'type': 'int', 'required': False, + 'default': 16}, + 'discovery_name': {'type': 'dict', 'required': False, + 'default': 'discovery_{}'.format(time.time())}, + 'netconf_port': {'type': 'str', 'required': False}, + 'password_list': {'type': 'list', 'required': False, + 'elements': 'str'}, + 'preferred_mgmt_ip_method': {'type': 'str', 'required': False, + 'default': 'None'}, + 'protocol_order': {'type': 'str', 'required': False}, + 'retry': {'type': 'int', 'required': False}, + 'snmp_auth_passphrase': {'type': 'str', 'required': False}, + 'snmp_auth_protocol': {'type': 'str', 'required': False}, + 'snmp_mode': {'type': 'str', 'required': False}, + 'snmp_priv_passphrase': {'type': 'str', 'required': False}, + 'snmp_priv_protocol': {'type': 'str', 'required': False}, + 'snmp_ro_community': {'type': 'str', 'required': False}, + 'snmp_ro_community_desc': {'type': 'str', 'required': False}, + 'snmp_rw_community': {'type': 'str', 'required': False}, + 'snmp_rw_community_desc': {'type': 'str', 'required': False}, + 'snmp_username': {'type': 'str', 'required': False}, + 'snmp_version': {'type': 'str', 'required': True}, + 'timeout': {'type': 'str', 'required': False}, + 'username_list': {'type': 'list', 'required': False, + 'elements': 'str'} + } + + # Validate discovery params + valid_discovery, invalid_params = validate_list_of_dicts( + self.config, discovery_spec + ) + if invalid_params: + self.msg = "Invalid parameters in playbook: {0}".format( + "\n".join(invalid_params)) + self.status = "failed" + return self + + self.validated_config = valid_discovery + self.log(str(valid_discovery)) + + self.msg = "Successfully validated input" + self.status = "success" + return self + + def get_creds_ids_list(self): + return self.creds_ids_list + + def get_dnac_global_credentials_v2_info(self): + response = self.dnac_apply['exec']( + family="discovery", + function='get_all_global_credentials_v2', + params=self.validated_config[0].get('headers'), + ) + response = response.get('response') + for value in response.values(): + if not value: + continue + self.creds_ids_list.extend(element.get('id') for element in value) + + if not self.creds_ids_list: + msg = 'Not found any credentials to discover' + self.module.fail_json(msg=msg) + + self.result.update(dict(credential_ids=self.creds_ids_list)) + + def get_devices_list_info(self): + devices_list = self.validated_config[0].get('devices_list') + self.result.update(dict(devices_info=devices_list)) + return devices_list + + def preprocessing_devices_info(self, devices_list: list): + ip_address_list = [device['ip'] for device in devices_list] + if self.validated_config[0].get('discovery_type') == "SINGLE": + ip_address_list = ip_address_list[0] + else: + ip_address_list = list( + map( + lambda x: f'{x}-{x}', + ip_address_list + ) + ) + ip_address_list = ','.join(ip_address_list) + return ip_address_list + + def create_params(self, credential_ids: None, ip_address_list: str = ''): + if credential_ids is None: + credential_ids = [] + new_object_params = {} + new_object_params['cdpLevel'] = self.validated_config[0].get('cdp_level') + new_object_params['discoveryType'] = self.validated_config[0].get('discovery_type') + new_object_params['enablePasswordList'] = self.validated_config[0].get( + 'enable_password_list') + new_object_params['globalCredentialIdList'] = credential_ids + new_object_params['httpReadCredential'] = self.validated_config[0].get( + 'http_read_credential') + new_object_params['httpWriteCredential'] = self.validated_config[0].get( + 'http_write_credential') + new_object_params['ipAddressList'] = ip_address_list + new_object_params['ipFilterList'] = self.validated_config[0].get('ip_filter_list') + new_object_params['lldpLevel'] = self.validated_config[0].get('lldp_level') + new_object_params['name'] = self.validated_config[0].get('discovery_name') + new_object_params['netconfPort'] = self.validated_config[0].get('netconf_port') + new_object_params['passwordList'] = self.validated_config[0].get('password_list') + new_object_params['preferredMgmtIPMethod'] = self.validated_config[0].get( + 'preferred_mgmt_ip_method') + new_object_params['protocolOrder'] = self.validated_config[0].get('protocol_order') + new_object_params['retry'] = self.validated_config[0].get('retry') + new_object_params['snmpAuthPassphrase'] = self.validated_config[0].get( + 'snmp_auth_Passphrase') + new_object_params['snmpAuthProtocol'] = self.validated_config[0].get( + 'snmp_auth_protocol') + new_object_params['snmpMode'] = self.validated_config[0].get('snmp_mode') + new_object_params['snmpPrivPassphrase'] = self.validated_config[0].get( + 'snmp_priv_passphrase') + new_object_params['snmpPrivProtocol'] = self.validated_config[0].get( + 'snmp_priv_protocol') + new_object_params['snmpROCommunity'] = self.validated_config[0].get( + 'snmp_ro_community') + new_object_params['snmpROCommunityDesc'] = self.validated_config[0].get( + 'snmp_ro_community_desc') + new_object_params['snmpRWCommunity'] = self.validated_config[0].get( + 'snmp_rw_community') + new_object_params['snmpRWCommunityDesc'] = self.validated_config[0].get( + 'snmp_rw_community_desc') + new_object_params['snmpUserName'] = self.validated_config[0].get( + 'snmp_username') + new_object_params['snmpVersion'] = self.validated_config[0].get('snmp_version') + new_object_params['timeout'] = self.validated_config[0].get('timeout') + new_object_params['userNameList'] = self.validated_config[0].get('user_name_list') + return new_object_params + + def create_discovery(self, credential_ids: list, ip_address_list: str): + result = self.dnac_apply['exec']( + family="discovery", + function="start_discovery", + params=self.create_params( + credential_ids=credential_ids, ip_address_list=ip_address_list), + op_modifies=True, + ) + + self.result.update(dict(discovery_result=result)) + return result.response.get('taskId') + + def get_task_status(self, task_id: str = None): + result = False + params = dict(task_id=task_id) + while True: + response = self.dnac_apply['exec']( + family="task", + function='get_task_by_id', + params=params, + ) + response = response.response + if response.get('isError') or re.search( + 'failed', response.get('progress'), flags=re.IGNORECASE + ): + msg = 'Discovery task with id {0} has not completed - Reason: {1}'.format( + task_id, response.get("failureReason")) + self.module.fail_json(msg=msg) + return False + + if response.get('progress') != 'In Progress': + result = True + break + time.sleep(3) + + self.result.update(dict(discovery_task=response)) + return result + + def lookup_discovery_by_range_via_name(self): + params = dict( + start_index=self.validated_config[0].get("start_index"), + records_to_return=self.validated_config[0].get("records_to_return"), + headers=self.validated_config[0].get("headers"), + ) + + response = self.dnac_apply['exec']( + family="discovery", + function='get_discoveries_by_range', + params=params + ) + return next( + filter( + lambda x: x['name'] == self.validated_config[0].get('discovery_name'), + response.response + ), None + ) + + def get_discoveries_by_range_until_success(self): + result = False + discovery = self.lookup_discovery_by_range_via_name() + if not discovery: + msg = 'Cannot find any discovery task with name {0} -- Discovery result: {1}'.format( + self.validated_config[0].get("discovery_name"), discovery) + self.module.fail_json(msg=msg) + + while True: + discovery = self.lookup_discovery_by_range_via_name() + if discovery.get('discoveryCondition') == 'Complete': + result = True + break + time.sleep(3) + + if not result: + msg = 'Cannot find any discovery task with name {0} -- Discovery result: {1}'.format( + self.validated_config[0].get("discovery_name"), discovery) + self.module.fail_json(msg=msg) + + self.result.update(dict(discovery_range=discovery)) + return discovery + + def get_discovery_device_info(self, discovery_id: str = None, task_id: str = None): + params = dict( + id=discovery_id, + task_id=task_id, + headers=self.validated_config[0].get("headers"), + ) + result = False + response = [] + count = 0 + while True: + response = self.dnac_apply['exec']( + family="discovery", + function='get_discovered_network_devices_by_discovery_id', + params=params, + ) + devices = response.response + if all(res.get('reachabilityStatus') == 'Success' for res in devices): + result = True + break + else: + count += 1 + + if count == 3: + break + + time.sleep(3) + + if not result: + msg = f'Discovery network device with id {discovery_id} has not completed' + self.module.fail_json(msg=msg) + + self.result.update(dict(discovery_device_info=devices)) + return result + + def get_exist_discovery(self): + discovery = self.lookup_discovery_by_range_via_name() + if not discovery: + self.result.update(dict(exist_discovery=discovery)) + return None + have = dict(exist_discovery=discovery) + self.have = have + self.result.update(dict(exist_discovery=discovery)) + return discovery + + def delete_exist_discovery(self, params): + response = self.dnac_apply['exec']( + family="discovery", + function="delete_discovery_by_id", + params=params, + ) + self.result.update(dict(delete_discovery=response)) + return response.response.get('taskId') + + def get_diff_merged(self): + self.get_dnac_global_credentials_v2_info() + devices_list_info = self.get_devices_list_info() + ip_address_list = self.preprocessing_devices_info(devices_list_info) + if exist_discovery := self.get_exist_discovery(): + params = dict(id=exist_discovery.get('id')) + discovery_task_id = self.delete_exist_discovery(params=params) + complete_discovery = self.get_task_status(task_id=discovery_task_id) + discovery_task_id = self.create_discovery( + credential_ids=self.get_creds_ids_list(), ip_address_list=ip_address_list) + complete_discovery = self.get_task_status(task_id=discovery_task_id) + discovery_task_info = self.get_discoveries_by_range_until_success() + result = self.get_discovery_device_info(discovery_id=discovery_task_info.get('id')) + self.result["changed"] = True + self.result['msg'] = "Discovery Created Successfully" + self.result['diff'] = self.validated_config + self.result['response'] = discovery_task_id + self.result.update(dict(msg='Discovery Created Successfully')) + return self + + def get_diff_deleted(self): + if exist_discovery := self.get_exist_discovery(): + params = dict(id=exist_discovery.get('id')) + discovery_task_id = self.delete_exist_discovery(params=params) + complete_discovery = self.get_task_status(task_id=discovery_task_id) + self.result["changed"] = True + self.result['msg'] = "Discovery Deleted Successfully" + self.result['diff'] = self.validated_config + self.result['response'] = discovery_task_id + else: + self.result['msg'] = "Discovery {0} Not Found".format( + self.validated_config[0].get("discovery_name")) + return self + + +def main(): + """ main entry point for module execution + """ + + element_spec = {'dnac_host': {'required': True, 'type': 'str'}, + 'dnac_port': {'type': 'str', 'default': '443'}, + 'dnac_username': {'type': 'str', 'default': 'admin', 'aliases': ['user']}, + 'dnac_password': {'type': 'str', 'no_log': True}, + 'dnac_verify': {'type': 'bool', 'default': 'True'}, + 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, + 'dnac_debug': {'type': 'bool', 'default': False}, + 'dnac_log': {'type': 'bool', 'default': False}, + 'validate_response_schema': {'type': 'bool', 'default': True}, + 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, + 'state': {'default': 'merged', 'choices': ['merged', 'deleted']} + } + + module = AnsibleModule(argument_spec=element_spec, + supports_check_mode=False) + + dnac_discovery = DnacDiscovery(module) + + state = dnac_discovery.params.get("state") + if state not in dnac_discovery.supported_states: + dnac_discovery.status = "invalid" + dnac_discovery.msg = "State {0} is invalid".format(state) + dnac_discovery.check_return_status() + + dnac_discovery.validate_input().check_return_status() + for config in dnac_discovery.validated_config: + dnac_discovery.get_diff_state_apply[state]().check_return_status() + + module.exit_json(**dnac_discovery.result) + + +if __name__ == '__main__': + main() From 560d6bccd9dbee2222ca57bd3820c65f96e35eca Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Thu, 30 Nov 2023 22:58:14 +0530 Subject: [PATCH 10/50] Updated Snakes, solved bugs and enhancement --- playbooks/device_credential_intent.yml | 112 +- playbooks/device_details.yml | 49 +- playbooks/network_settings_intent.yml | 109 +- playbooks/template_pnp_intent.yml | 19 +- plugins/module_utils/dnac.py | 9 +- plugins/modules/device_credential_intent.py | 591 +++--- plugins/modules/network_settings_intent.py | 754 +++---- plugins/modules/template_intent.py | 2125 +++++++++++++------ 8 files changed, 2307 insertions(+), 1461 deletions(-) diff --git a/playbooks/device_credential_intent.yml b/playbooks/device_credential_intent.yml index 13dbbb3425..03952d168d 100644 --- a/playbooks/device_credential_intent.yml +++ b/playbooks/device_credential_intent.yml @@ -19,67 +19,95 @@ dnac_log: True state: merged config: - - GlobalCredentialDetails: - cliCredential: - - description: CLI - username: cli + - global_credential_details: + cli_credential: + - description: CLI1 + username: cli1 password: "12345" - enablePassword: "12345" + enable_password: "12345" # old_description: # old_username: # id: e448ea13-4de0-406b-bc6e-f72b57ed6746 # Use this for updation or deletion - snmpV2cRead: - - description: SNMPv2c Read # use this for deletion - readCommunity: "12345" + snmp_v2c_read: + - description: SNMPv2c Read1 # use this for deletion + read_community: "123456" # old_description: # use this for updating the description # id: 0ee7d677-8804-43f2-8b6c-599c5f18348f # Use this for updation or deletion - snmpV2cWrite: - - description: SNMPv2c Write # use this for deletion - writeCommunity: "12345" + snmp_v2c_write: + - description: SNMPv2c Write1 # use this for deletion + write_community: "123456" # old_description: # use this for updating the description # id: a96abc1b-1fd6-41f1-8a6d-a5569c17262d # Use this for updation or deletion - snmpV3: - - authPassword: "12345678" # Atleast 8 characters - authType: SHA # [SHA, MD5] (SHA is recommended) - snmpMode: AUTHPRIV # [AUTHPRIV, AUTHNOPRIV, NOAUTHNOPRIV] - privacyPassword: "12345678" # Atleast 8 characters - privacyType: AES128 # [AE128, AE192, AE256] - username: snmpV3 - description: snmpV3 + snmp_v3: + - auth_password: "12345678" # Atleast 8 characters + auth_type: SHA # [SHA, MD5] (SHA is recommended) + snmp_mode: AUTHPRIV # [AUTHPRIV, AUTHNOPRIV, NOAUTHNOPRIV] + privacy_password: "12345678" # Atleast 8 characters + privacy_type: AES128 # [AE128, AE192, AE256] + username: snmpV31 + description: snmpV31 # old_description: # id: d8974823-250a-41b0-8c9b-b27b2ae01472 # Use this for updation or deletion - httpsRead: - - description: HTTP Read - username: HTTP_Read + https_read: + - description: HTTP Read1 + username: HTTP_Read1 password: "12345" port: 443 # old_description: # old_username: # id: a7ef9995-e404-4240-94ca-b5f37f65c19d # Use this for updation or deletion - httpsWrite: - - description: HTTP Write - username: HTTP_Write + https_write: + - description: HTTP Write1 + username: HTTP_Write1 password: "12345" port: 443 # old_description: # old_username: # id: bec9818e-30cd-468b-bf75-292beefc2e20 # Use this for updation or deletion - AssignCredentialsToSite: - # cliDescription: - # cliUsername: - cliId: e448ea13-4de0-406b-bc6e-f72b57ed6746 - # snmpV2ReadDescription: - snmpV2ReadId: 0ee7d677-8804-43f2-8b6c-599c5f18348f - # snmpV2WriteDescription: - snmpV2WriteId: a96abc1b-1fd6-41f1-8a6d-a5569c17262d - # snmpV3Description: - snmpV3Id: d8974823-250a-41b0-8c9b-b27b2ae01472 - # httpReadDescription: - # httpReadUsername: - httpRead: d5d7af00-5a38-4ac1-9f55-03338d00c415 - # httpWriteDescription: - # httpWriteUsername: - httpWrite: bec9818e-30cd-468b-bf75-292beefc2e20 - siteName: + assign_credentials_to_site: + # cli_description: + # cli_username: + cli_id: e448ea13-4de0-406b-bc6e-f72b57ed6746 + # snmp_v2_read_description: + snmp_v2_read_id: 0ee7d677-8804-43f2-8b6c-599c5f18348f + # snmp_v2_write_description: + snmp_v2_write_id: a96abc1b-1fd6-41f1-8a6d-a5569c17262d + # snmp_v3_description: + snmp_v3_id: d8974823-250a-41b0-8c9b-b27b2ae01472 + # http_read_description: + # http_read_username: + http_read: d5d7af00-5a38-4ac1-9f55-03338d00c415 + # http_write_description: + # http_write_username: + http_write: bec9818e-30cd-468b-bf75-292beefc2e20 + site_name: - Global/Chennai/Trill - Global/Chennai/Tidel + + - name: Delete Credentials + cisco.dnac.device_credential_intent: + dnac_host: "{{ dnac_host }}" + dnac_port: "{{ dnac_port }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_debug: "{{ dnac_debug }}" + dnac_log: True + state: deleted + config: + - global_credential_details: + cli_credential: + - description: CLI1 + username: cli1 + snmp_v2c_read: + - description: SNMPv2c Read1 # use this for deletion + snmp_v2c_write: + - description: SNMPv2c Write1 # use this for deletion + snmp_v3: + - description: snmpV31 + https_read: + - description: HTTP Read1 + username: HTTP_Read1 + https_write: + - description: HTTP Write1 + username: HTTP_Write1 diff --git a/playbooks/device_details.yml b/playbooks/device_details.yml index 0c9dfdfef4..ae4d017ffd 100644 --- a/playbooks/device_details.yml +++ b/playbooks/device_details.yml @@ -6,23 +6,38 @@ template_details: type: "IOS-XE" variant: "XE" temp_name: "temp_cat9k-1" - description: "Test Template 1" - - proj_name: "Onboarding Configuration" - device_config: "hostname cat9k-2\n" - language: "velocity" - family: "Switches and Hubs" - type: "IOS-XE" - variant: "XE" - temp_name: "temp_cat9k-2" - description: "Test Template 2" - - proj_name: "Onboarding Configuration" - device_config: "hostname cat9k-3\n" - language: "velocity" - family: "Switches and Hubs" - type: "IOS-XE" - variant: "XE" - temp_name: "temp_cat9k-3" - description: "Test Template 3" + description: "Test Template" + export_project: + - "Cloud DayN Templates" + export_template: + - project_name: "Cloud DayN Templates" + template_name: "DMVPN Spoke for Branch Router - System Default" + import_project: + do_version: false + payload: + - name: "Onboarding Configuration2" + import_template: + do_version: false + project_name: "Onboarding Configuration" + payload: + - name: "Platinum-Onboarding-Template-J21" + device_types: + - product_family: "Switches and Hubs" + productSeries: "Cisco Catalyst 9300 Series Switches" + software_type: "IOS" + language: "JINJA" + - name: "Platinum-Onboarding-Template-J22" + device_types: + - product_family: "Switches and Hubs" + productSeries: "Cisco Catalyst 9300 Series Switches" + software_type: "IOS" + language: "JINJA" + - name: "Platinum-Onboarding-Template-J23" + device_types: + - product_family: "Switches and Hubs" + productSeries: "Cisco Catalyst 9300 Series Switches" + software_type: "IOS" + language: "JINJA" device_details: - site_name: "Global/Chennai/Trill" diff --git a/playbooks/network_settings_intent.yml b/playbooks/network_settings_intent.yml index 58220da335..14062f32c7 100644 --- a/playbooks/network_settings_intent.yml +++ b/playbooks/network_settings_intent.yml @@ -19,68 +19,89 @@ dnac_log: True state: merged config: - - GlobalPoolDetails: + - global_pool_details: settings: ippool: - - ipPoolName: Global_Pool2 + - ip_pool_name: Global_Pool2 gateway: "" #use this for updating - IpAddressSpace: IPv6 #required when we are creating - ipPoolCidr: 2001:db8::/64 #required when we are creating + ip_address_space: IPv6 #required when we are creating + ip_pool_cidr: 2001:db8::/64 #required when we are creating type: Generic - dhcpServerIps: [] #use this for updating - dnsServerIps: [] #use this for updating + dhcp_server_ips: [] #use this for updating + dns_server_ips: [] #use this for updating # prev_name: Global_Pool2 - ReservePoolDetails: - ipv6AddressSpace: True - ipv4GlobalPool: 100.0.0.0/8 - ipv4Prefix: True - ipv4PrefixLength: 9 - ipv4Subnet: 100.128.0.0 - # ipv4DnsServers: [100.128.0.1] + reserve_pool_details: + ipv6_address_space: True + ipv4_global_pool: 100.0.0.0/8 + ipv4_prefix: True + ipv4_prefix_length: 9 + ipv4_subnet: 100.128.0.0 + # ipv4_dns_servers: [100.128.0.1] name: IP_Pool_3 - ipv6Prefix: True - ipv6PrefixLength: 64 - ipv6GlobalPool: 2001:db8::/64 - ipv6Subnet: "2001:db8::" - siteName: Global/Chennai/Trill - slaacSupport: True + ipv6_prefix: True + ipv6_prefix_length: 64 + ipv6_global_pool: 2001:db8::/64 + ipv6_subnet: "2001:db8::" + site_name: Global/Chennai/Trill + slaac_support: True # prev_name: IP_Pool_4 type: LAN - NetworkManagementDetails: + network_management_details: settings: - dhcpServer: + dhcp_server: - 10.0.0.1 - dnsServer: - domainName: cisco.com - primaryIpAddress: 10.0.0.2 - secondaryIpAddress: 10.0.0.3 - clientAndEndpoint_aaa: #works only if we system settigns is set - # ipAddress: 10.197.156.42 #Mandatory for ISE, sec ip for AAA + dns_server: + domain_name: cisco.com + primary_ip_address: 10.0.0.2 + secondary_ip_address: 10.0.0.3 + 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 protocol: RADIUS servers: AAA - # sharedSecret: string #ISE - messageOfTheday: - bannerMessage: hello - retainExistingBanner: "true" - netflowcollector: - ipAddress: 10.0.0.4 + # shared_secret: string #ISE + message_of_the_day: + banner_message: hello + retain_existing_banner: "true" + netflow_collector: + ip_address: 10.0.0.4 port: 443 network_aaa: #works only if we system settigns is set - # ipAddress: string #Mandatory for ISE, sec ip for AAA + ip_address: 10.0.0.21 #Mandatory for ISE, sec ip for AAA network: 10.0.0.20 protocol: TACACS servers: AAA - # sharedSecret: string #ISE - ntpServer: + # shared_secret: string #ISE + ntp_server: - 10.0.0.5 - snmpServer: - configureDnacIP: True - ipAddresses: - - 10.0.0.6 + snmp_server: + configure_dnac_ip: false + # ip_addresses: + # - 10.0.0.6 syslogServer: - configureDnacIP: True - ipAddresses: - - 10.0.0.7 + configure_dnac_ip: false + # ip_addresses: + # - 10.0.0.7 timezone: GMT - siteName: Global/Chennai + site_name: Global/Chennai + + - name: Delete Global Pool and Release Pool Reservation + cisco.dnac.network_settings_intent: + dnac_host: "{{ dnac_host }}" + dnac_port: "{{ dnac_port }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_debug: "{{ dnac_debug }}" + dnac_log: True + state: deleted + config: + - global_pool_details: + settings: + ippool: + - ip_pool_name: Global_Pool2 + reserve_pool_details: + name: IP_Pool_3 + site_name: Global/Chennai/Trill + + diff --git a/playbooks/template_pnp_intent.yml b/playbooks/template_pnp_intent.yml index 4ce25e16d0..61e870f114 100644 --- a/playbooks/template_pnp_intent.yml +++ b/playbooks/template_pnp_intent.yml @@ -20,15 +20,16 @@ state: "merged" #ignore_errors: true #Enable this to continue execution even the task fails config: - - projectName: "{{ item.proj_name }}" - templateContent: "{{ item.device_config }}" - language: "{{ item.language }}" - deviceTypes: - - productFamily: "{{ item.family }}" - softwareType: "{{ item.type }}" - softwareVariant: "{{ item.variant }}" - templateName: "{{ item.temp_name }}" - versionDescription: "{{ item.description }}" + - configuration_templates: + project_name: "{{ item.proj_name }}" + template_content: "{{ item.device_config }}" + language: "{{ item.language }}" + device_types: + - productFamily: "{{ item.family }}" + software_type: "{{ item.type }}" + software_variant: "{{ item.variant }}" + template_name: "{{ item.temp_name }}" + version_description: "{{ item.description }}" register: template_result with_items: '{{ template_details }}' tags: diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 98290e5dd1..d4fb16cc0a 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -158,7 +158,7 @@ def get_task_details(self, task_id): return result - def check_task_response_status(self, response, validation_string): + def check_task_response_status(self, response, validation_string, data=False): """ Get the site id from the site name. @@ -187,12 +187,17 @@ def check_task_response_status(self, response, validation_string): self.log(str(task_details)) if task_details.get("isError") is True: - self.msg = str(task_details.get("progress")) + if task_details.get("failureReason"): + self.msg = str(task_details.get("failureReason")) + else: + self.msg = str(task_details.get("progress")) self.status = "failed" break if validation_string in task_details.get("progress").lower(): self.result['changed'] = True + if data == True: + self.msg = task_details.get("data") self.status = "success" break diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index 01f9bc877c..a95be4b8ac 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -19,7 +19,7 @@ - API to update global device credentials. - API to delete global device credentials. - API to assign the device credential to the site. -version_added: '6.8.0' +version_added: '6.7.0' extends_documentation_fragment: - cisco.dnac.intent_params author: Muthu Rakesh (@MUTHU-RAKESH-27) @@ -37,11 +37,11 @@ elements: dict required: true suboptions: - GlobalCredentialDetails: + global_credential_details: description: Manages global device credentials type: dict suboptions: - cliCredential: + cli_credential: description: Global Credential V2's cliCredential. type: list elements: dict @@ -49,9 +49,9 @@ description: description: Description. Required for creating the credential. type: str - enablePassword: + enable_password: description: - - cliCredential credential Enable Password. + - cli_credential credential Enable Password. - Password cannot contain spaces or angle brackets (< >) type: str id: @@ -59,13 +59,13 @@ type: str password: description: - - cliCredential credential Password. + - cli_credential credential Password. - Required for creating/updating the credential. - Password cannot contain spaces or angle brackets (< >). type: str username: description: - - cliCredential credential Username. + - cli_credential credential Username. - Username cannot contain spaces or angle brackets (< >). type: str old_description: @@ -74,7 +74,7 @@ old_username: description: Old Username. Use this for updating the description/Username. type: str - httpsRead: + https_read: description: Global Credential V2's httpsRead. type: list elements: dict @@ -87,7 +87,7 @@ type: str password: description: - - httpsRead credential Password. + - https_read credential Password. - Required for creating/updating the credential. - Password cannot contain spaces or angle brackets (< >). type: str @@ -96,7 +96,7 @@ type: int username: description: - - httpsRead credential Username. + - https_read credential Username. - Username cannot contain spaces or angle brackets (< >). type: str old_description: @@ -105,7 +105,7 @@ old_username: description: Old Username. Use this for updating the description/Username. type: str - httpsWrite: + https_write: description: Global Credential V2's httpsWrite. type: list elements: dict @@ -118,7 +118,7 @@ type: str password: description: - - httpsWrite credential Password. + - https_write credential Password. - Required for creating/updating the credential. - Password cannot contain spaces or angle brackets (< >). type: str @@ -127,7 +127,7 @@ type: int username: description: - - httpsWrite credential Username. + - https_write credential Username. - Username cannot contain spaces or angle brackets (< >). type: str old_description: @@ -136,7 +136,7 @@ old_username: description: Old Username. Use this for updating the description/Username. type: str - snmpV2cRead: + snmp_v2c_read: description: Global Credential V2's snmpV2cRead. type: list elements: dict @@ -147,15 +147,15 @@ id: description: Credential Id. Use this for updating the device credential. type: str - readCommunity: + read_community: description: - - snmpV2cRead Read Community. + - snmp_v2c_read Read Community. - Password cannot contain spaces or angle brackets (< >). type: str old_description: description: Old Description. Use this for updating the description. type: str - snmpV2cWrite: + snmp_v2c_write: description: Global Credential V2's snmpV2cWrite. type: list elements: dict @@ -166,108 +166,108 @@ id: description: Credential Id. Use this for updating the device credential. type: str - writeCommunity: + write_community: description: - - snmpV2cWrite Write Community. + - snmp_v2c_write Write Community. - Password cannot contain spaces or angle brackets (< >). type: str old_description: description: Old Description. Use this for updating the description. type: str - snmpV3: + snmp_v3: description: Global Credential V2's snmpV3. type: list elements: dict suboptions: - authPassword: + auth_password: description: - - snmpV3 Auth Password. + - snmp_v3 Auth Password. - Password must contain minimum 8 characters. - Password cannot contain spaces or angle brackets (< >). type: str - authType: + auth_type: description: Auth Type. ["SHA", "MD5"]. type: str description: description: - - snmpV3 Description. - - Should be unique from other snmpV3 credentials. + - snmp_v3 Description. + - Should be unique from other snmp_v3 credentials. type: str id: description: Credential Id. Use this for updating the device credential. type: str - privacyPassword: + privacy_password: description: - - snmpV3 Privacy Password. + - snmp_v3 Privacy Password. - Password must contain minimum 8 characters. - Password cannot contain spaces or angle brackets (< >). type: str - privacyType: + privacy_type: description: Privacy Type. ["AES128", "AES192", "AES256"]. type: str - snmpMode: + snmp_mode: description: Snmp Mode. ["AUTHPRIV", "AUTHNOPRIV", "NOAUTHNOPRIV"]. type: str username: description: - - snmpV3 credential Username. + - snmp_v3 credential Username. - Username cannot contain spaces or angle brackets (< >). type: str old_description: description: Old Description. Use this for updating the description. type: str - AssignCredentialsToSite: + assign_credentials_to_site: description: Assign Device Credentials to Site. type: dict suboptions: - cliDescription: + cli_description: description: CLI Credential Description. type: str - cliUsername: + cli_username: description: CLI Credential Username. type: str - cliId: + cli_id: description: CLI Credential Id. Use (Description, Username) or Id. type: str - httpReadDescription: + http_read_description: description: HTTP(S) Read Credential Description. type: str - httpReadUsername: + http_read_username: description: HTTP(S) Read Credential Username. type: str - httpRead: + http_read: description: HTTP(S) Read Credential Id. Use (Description, Username) or Id. type: str - httpWriteDescription: + http_write_description: description: HTTP(S) Write Credential Description. type: str - httpWriteUsername: + http_write_username: description: HTTP(S) Write Credential Username. type: str - httpWrite: + http_write: description: HTTP(S) Write Credential Id. Use (Description, Username) or Id. type: str - siteName: + site_name: description: Site Name to assign credential. type: list elements: str - snmpV2ReadDescription: + snmp_v2_read_description: description: SNMPv2c Read Credential Description. type: str - snmpV2ReadId: + snmp_v2_read_id: description: SNMPv2c Read Credential Id. Use Description or Id. type: str - snmpV2WriteDescription: + snmp_v2_write_description: description: SNMPv2c Write Credential Description. type: str - snmpV2WriteId: + snmp_v2_write_id: description: SNMPv2c Write Credential Id. Use Description or Id. type: str - snmpV3Description: - description: SNMPv3 Credential Description. + snmp_v3_description: + description: snmp_v3 Credential Description. type: str - snmpV3Id: - description: SNMPv3 Credential Id. Use Description or Id. + snmp_v3_id: + description: snmp_v3 Credential Id. Use Description or Id. type: str requirements: - dnacentersdk >= 2.5.5 @@ -312,44 +312,44 @@ dnac_log: True state: merged config: - - GlobalCredentialDetails: - cliCredential: + - global_credential_details: + cli_credential: - description: string username: string password: string - enablePassword: string - snmpV2cRead: + enable_password: string + snmp_v2c_read: - description: string - readCommunity: string - snmpV2cWrite: + read_community: string + snmp_v2c_write: - description: string - writeCommunity: string - snmpV3: - - authPassword: string - authType: SHA - snmpMode: AUTHPRIV - privacyPassword: string - privacyType: AES128 + write_community: string + snmp_v3: + - auth_password: string + auth_type: SHA + snmp_mode: AUTHPRIV + privacy_password: string + privacy_type: AES128 username: string description: string - httpsRead: + https_read: - description: string username: string password: string port: 443 - httpsWrite: + https_write: - description: string username: string password: string port: 443 - AssignCredentialsToSite: - cliId: string - snmpV2ReadId: string - snmpV2WriteId: string - snmpV3Id: string - httpRead: string - httpWrite: string - siteName: + assign_credentials_to_site: + cli_id: string + snmp_v2_read_id: string + snmp_v2_write_id: string + snmp_v3_id: string + http_read: string + http_write: string + site_name: - string - name: Create Multiple Credentials. @@ -363,42 +363,42 @@ dnac_log: True state: merged config: - - GlobalCredentialDetails: - cliCredential: + - global_credential_details: + cli_credential: - description: string username: string password: string - enablePassword: string + enable_password: string - description: string username: string password: string - enablePassword: string - snmpV2cRead: + enable_password: string + snmp_v2c_read: - description: string - readCommunity: string + read_community: string - description: string - readCommunity: string - snmpV2cWrite: + read_community: string + snmp_v2c_write: - description: string - writeCommunity: string + write_community: string - description: string - writeCommunity: string - snmpV3: - - authPassword: string - authType: SHA - snmpMode: AUTHPRIV - privacyPassword: string - privacyType: AES128 + write_community: string + snmp_v3: + - auth_password: string + auth_type: SHA + snmp_mode: AUTHPRIV + privacy_password: string + privacy_type: AES128 username: string description: string - - authPassword: string - authType: SHA - snmpMode: AUTHPRIV - privacyPassword: string - privacyType: AES128 + - auth_password: string + auth_type: SHA + snmp_mode: AUTHPRIV + privacy_password: string + privacy_type: AES128 username: string description: string - httpsRead: + https_read: - description: string username: string password: string @@ -407,7 +407,7 @@ username: string password: string port: 443 - httpsWrite: + https_write: - description: string username: string password: string @@ -428,37 +428,37 @@ dnac_log: True state: merged config: - - GlobalCredentialDetails: - cliCredential: + - global_credential_details: + cli_credential: - description: string username: string password: string - enablePassword: string + enable_password: string id: string - snmpV2cRead: + snmp_v2c_read: - description: string - readCommunity: string + read_community: string id: string - snmpV2cWrite: + snmp_v2c_write: - description: string - writeCommunity: string + write_community: string id: string - snmpV3: - - authPassword: string - authType: SHA - snmpMode: AUTHPRIV - privacyPassword: string - privacyType: AES128 + snmp_v3: + - auth_password: string + auth_type: SHA + snmp_mode: AUTHPRIV + privacy_password: string + privacy_type: AES128 username: string description: string id: string - httpsRead: + https_read: - description: string username: string password: string port: 443 id: string - httpsWrite: + https_write: - description: string username: string password: string @@ -476,50 +476,50 @@ dnac_log: True state: merged config: - - GlobalCredentialDetails: - cliCredential: + - global_credential_details: + cli_credential: - description: string username: string password: string - enablePassword: string + enable_password: string id: string - description: string username: string password: string - enablePassword: string + enable_password: string id: string - snmpV2cRead: + snmp_v2c_read: - description: string - readCommunity: string + read_community: string id: string - description: string - readCommunity: string + read_community: string id: string - snmpV2cWrite: + snmp_v2c_write: - description: string - writeCommunity: string + write_community: string id: string - description: string - writeCommunity: string + write_community: string id: string - snmpV3: - - authPassword: string - authType: SHA - snmpMode: AUTHPRIV - privacyPassword: string - privacyType: AES128 + snmp_v3: + - auth_password: string + auth_type: SHA + snmp_mode: AUTHPRIV + privacy_password: string + privacy_type: AES128 username: string description: string id: string - - authPassword: string - authType: SHA - snmpMode: AUTHPRIV - privacyPassword: string - privacyType: AES128 + - auth_password: string + auth_type: SHA + snmp_mode: AUTHPRIV + privacy_password: string + privacy_type: AES128 username: string description: string id: string - httpsRead: + https_read: - description: string username: string password: string @@ -530,7 +530,7 @@ password: string port: 443 id: string - httpsWrite: + https_write: - description: string username: string password: string @@ -553,38 +553,38 @@ dnac_log: True state: merged config: - - GlobalCredentialDetails: - cliCredential: + - global_credential_details: + cli_credential: - description: string username: string password: string - enablePassword: string + enable_password: string old_description: string old_username: string - snmpV2cRead: + snmp_v2c_read: - description: string - readCommunity: string + read_community: string old_description: string - snmpV2cWrite: + snmp_v2c_write: - description: string - writeCommunity: string + write_community: string old_description: string - snmpV3: - - authPassword: string - authType: string - snmpMode: string - privacyPassword: string - privacyType: string + snmp_v3: + - auth_password: string + auth_type: string + snmp_mode: string + privacy_password: string + privacy_type: string username: string description: string - httpsRead: + https_read: - description: string username: string password: string port: string old_description: string old_username: string - httpsWrite: + https_write: - description: string username: string password: string @@ -603,17 +603,17 @@ dnac_log: True state: merged config: - - AssignCredentialsToSite: - cliDescription: string - cliUsername: string - snmpV2ReadDescription: string - snmpV2WriteDescription: string - snmpV3Description: string - httpReadDescription: string - httpReadUsername: string - httpWriteUsername: string - httpWriteDescription: string - siteName: + - assign_credentials_to_site: + cli_description: string + cli_username: string + snmp_v2_read_description: string + snmp_v2_write_description: string + snmp_v3_description: string + http_read_description: string + http_read_username: string + http_write_username: string + http_write_description: string + site_name: - string - string @@ -649,6 +649,7 @@ } """ +import copy from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( DnacBase, @@ -696,45 +697,45 @@ def validate_input(self): # temp_spec is the specification for the expected structure of configuration parameters temp_spec = { - "GlobalCredentialDetails": { + "global_credential_details": { "type": 'dict', - "cliCredential": { + "cli_credential": { "type": 'list', "description": {"type": 'string'}, "username": {"type": 'string'}, "password": {"type": 'string'}, - "enablePassword": {"type": 'string'}, + "enable_password": {"type": 'string'}, "old_description": {"type": 'string'}, "old_username": {"type": 'string'}, "id": {"type": 'string'}, }, - "snmpV2cRead": { + "snmp_v2c_read": { "type": 'list', "description": {"type": 'string'}, - "readCommunity": {"type": 'string'}, + "read_community": {"type": 'string'}, "old_description": {"type": 'string'}, "id": {"type": 'string'}, }, - "snmpV2cWrite": { + "snmp_v2c_write": { "type": 'list', "description": {"type": 'string'}, - "writeCommunity": {"type": 'string'}, + "write_community": {"type": 'string'}, "old_description": {"type": 'string'}, "id": {"type": 'string'}, }, - "snmpV3": { + "snmp_v3": { "type": 'list', "description": {"type": 'string'}, "username": {"type": 'string'}, - "snmpMode": {"type": 'string'}, - "authType": {"type": 'string'}, - "authPassword": {"type": 'string'}, - "privacyType": {"type": 'string'}, - "privacyPassword": {"type": 'string'}, + "snmp_mode": {"type": 'string'}, + "auth_type": {"type": 'string'}, + "auth_password": {"type": 'string'}, + "privacy_type": {"type": 'string'}, + "privacy_password": {"type": 'string'}, "old_description": {"type": 'string'}, "id": {"type": 'string'}, }, - "httpsRead": { + "https_read": { "type": 'list', "description": {"type": 'string'}, "username": {"type": 'string'}, @@ -744,7 +745,7 @@ def validate_input(self): "old_username": {"type": 'string'}, "id": {"type": 'string'}, }, - "httpsWrite": { + "https_write": { "type": 'list', "description": {"type": 'string'}, "username": {"type": 'string'}, @@ -755,24 +756,24 @@ def validate_input(self): "id": {"type": 'string'}, } }, - "AssignCredentialsToSite": { + "assign_credentials_to_site": { "type": 'dict', - "cliDescription": {"type": 'string'}, - "cliUsername": {"type": 'string'}, - "cliId": {"type": 'string'}, - "snmpV2ReadDescription": {"type": 'string'}, - "snmpV2ReadId": {"type": 'string'}, - "snmpV2WriteDescription": {"type": 'string'}, - "snmpV2WriteId": {"type": 'string'}, - "snmpV3Description": {"type": 'string'}, - "snmpV3Id": {"type": 'string'}, - "httpReadDescription": {"type": 'string'}, - "httpReadUsername": {"type": 'string'}, - "httpRead": {"type": 'string'}, - "httpWriteDescription": {"type": 'string'}, - "httpWriteUsername": {"type": 'string'}, - "httpWrite": {"type": 'string'}, - "siteName": {"type": 'list'} + "cli_description": {"type": 'string'}, + "cli_username": {"type": 'string'}, + "cli_id": {"type": 'string'}, + "snmp_v2_read_description": {"type": 'string'}, + "snmp_v2_read_id": {"type": 'string'}, + "snmp_v2_write_description": {"type": 'string'}, + "snmp_v2_write_id": {"type": 'string'}, + "snmp_v3_description": {"type": 'string'}, + "snmp_v3_id": {"type": 'string'}, + "http_read_description": {"type": 'string'}, + "http_read_username": {"type": 'string'}, + "http_read": {"type": 'string'}, + "http_write_description": {"type": 'string'}, + "http_write_username": {"type": 'string'}, + "http_write": {"type": 'string'}, + "site_name": {"type": 'list'} } } @@ -997,7 +998,7 @@ def get_snmpV3_params(self, snmpV3Details): value = { "username": item.get("username"), "description": item.get("description"), - "snmpMode": item.get("snmpMode"), + "snmpMode": item.get("snmp_mode"), "id": item.get("id"), } if value.get("snmpMode") == "AUTHNOPRIV": @@ -1025,7 +1026,7 @@ def get_cli_credentials(self, CredentialDetails, global_credentials): """ # playbook CLI Credential details - all_CLI = CredentialDetails.get("cliCredential") + all_CLI = CredentialDetails.get("cli_credential") # All CLI details from Cisco DNA Center cli_details = global_credentials.get("cliCredential") # Cisco DNA Center details for the CLI Credential given in the playbook @@ -1088,7 +1089,7 @@ def get_snmpV2cRead_credentials(self, CredentialDetails, global_credentials): """ # Playbook snmpV2cRead Credential details - all_snmpV2cRead = CredentialDetails.get("snmpV2cRead") + all_snmpV2cRead = CredentialDetails.get("snmp_v2c_read") # All snmpV2cRead details from the Cisco DNA Center snmpV2cRead_details = global_credentials.get("snmpV2cRead") # Cisco DNA Center details for the snmpV2cRead Credential given in the playbook @@ -1141,7 +1142,7 @@ def get_snmpV2cWrite_credentials(self, CredentialDetails, global_credentials): """ # Playbook snmpV2cWrite Credential details - all_snmpV2cWrite = CredentialDetails.get("snmpV2cWrite") + all_snmpV2cWrite = CredentialDetails.get("snmp_v2c_write") # All snmpV2cWrite details from the Cisco DNA Center snmpV2cWrite_details = global_credentials.get("snmpV2cWrite") # Cisco DNA Center details for the snmpV2cWrite Credential given in the playbook @@ -1194,7 +1195,7 @@ def get_httpsRead_credentials(self, CredentialDetails, global_credentials): """ # Playbook httpsRead Credential details - all_httpsRead = CredentialDetails.get("httpsRead") + all_httpsRead = CredentialDetails.get("https_read") # All httpsRead details from the Cisco DNA Center httpsRead_details = global_credentials.get("httpsRead") # Cisco DNA Center details for the httpsRead Credential given in the playbook @@ -1257,7 +1258,7 @@ def get_httpsWrite_credentials(self, CredentialDetails, global_credentials): """ # Playbook httpsWrite Credential details - all_httpsWrite = CredentialDetails.get("httpsWrite") + all_httpsWrite = CredentialDetails.get("https_write") # All httpsWrite details from the Cisco DNA Center httpsWrite_details = global_credentials.get("httpsWrite") # Cisco DNA Center details for the httpsWrite Credential given in the playbook @@ -1316,7 +1317,7 @@ def get_snmpV3_credentials(self, CredentialDetails, global_credentials): """ # Playbook snmpV3 Credential details - all_snmpV3 = CredentialDetails.get("snmpV3") + all_snmpV3 = CredentialDetails.get("snmp_v3") # All snmpV3 details from the Cisco DNA Center snmpV3_details = global_credentials.get("snmpV3") # Cisco DNA Center details for the snmpV3 Credential given in the playbook @@ -1408,8 +1409,8 @@ def get_have(self, config): Device Credentials and Device Credentials assigned to a site. """ - if config.get("GlobalCredentialDetails") is not None: - CredentialDetails = config.get("GlobalCredentialDetails") + if config.get("global_credential_details") is not None: + CredentialDetails = config.get("global_credential_details") self.get_have_device_credentials(CredentialDetails).check_return_status() self.log("Credentials and Credentials Assigned to Site Details in Cisco DNA Center " + @@ -1435,8 +1436,8 @@ def get_want_device_credentials(self, CredentialDetails): "want_create": {}, "want_update": {} } - if CredentialDetails.get("cliCredential"): - cli = CredentialDetails.get("cliCredential") + if CredentialDetails.get("cli_credential"): + cli = CredentialDetails.get("cli_credential") have_cli_ptr = 0 create_cli_ptr = 0 update_cli_ptr = 0 @@ -1458,9 +1459,9 @@ def get_want_device_credentials(self, CredentialDetails): self.status = "failed" return self - if item.get("enablePassword"): + if item.get("enable_password"): create_credential[create_cli_ptr] \ - .update({"enablePassword": item.get("enablePassword")}) + .update({"enablePassword": item.get("enable_password")}) create_cli_ptr = create_cli_ptr + 1 else: if want.get("want_update").get("cliCredential") is None: @@ -1486,19 +1487,20 @@ def get_want_device_credentials(self, CredentialDetails): .get("cliCredential")[have_cli_ptr].get(values[i]) }) - if item.get("enablePassword"): + if item.get("enable_password"): update_credential[update_cli_ptr].update({ - "enablePassword": item.get("enablePassword") + "enablePassword": item.get("enable_password") }) update_cli_ptr = update_cli_ptr + 1 have_cli_ptr = have_cli_ptr + 1 - if CredentialDetails.get("snmpV2cRead"): - snmpV2cRead = CredentialDetails.get("snmpV2cRead") + if CredentialDetails.get("snmp_v2c_read"): + snmpV2cRead = CredentialDetails.get("snmp_v2c_read") have_snmpv2cread_ptr = 0 create_snmpv2cread_ptr = 0 update_snmpv2cread_ptr = 0 - values = ["readCommunity", "description", "id"] + values = ["read_community", "description", "id"] + keys = ["readCommunity", "description", "id"] have_snmpV2cRead = self.have.get("globalCredential").get("snmpV2cRead") for item in snmpV2cRead: if not have_snmpV2cRead or have_snmpV2cRead[have_snmpv2cread_ptr] is None: @@ -1509,7 +1511,7 @@ def get_want_device_credentials(self, CredentialDetails): for i in range(0, 2): if item.get(values[i]): create_credential[create_snmpv2cread_ptr] \ - .update({values[i]: item.get(values[i])}) + .update({keys[i]: item.get(values[i])}) else: self.msg = values[i] + " is mandatory for creating \ snmpV2cRead " + str(have_snmpv2cread_ptr) @@ -1521,11 +1523,11 @@ def get_want_device_credentials(self, CredentialDetails): want.get("want_update").update({"snmpV2cRead": []}) update_credential = want.get("want_update").get("snmpV2cRead") update_credential.append({}) - if item.get("readCommunity"): + if item.get("read_community"): update_credential[update_snmpv2cread_ptr] \ - .update({"readCommunity": item.get("readCommunity")}) + .update({"readCommunity": item.get("read_community")}) else: - self.msg = "readCommunity is mandatory for updating \ + self.msg = "read_community is mandatory for updating \ snmpV2cRead " + str(have_snmpv2cread_ptr) self.status = "failed" return self @@ -1541,12 +1543,13 @@ def get_want_device_credentials(self, CredentialDetails): update_snmpv2cread_ptr = update_snmpv2cread_ptr + 1 have_snmpv2cread_ptr = have_snmpv2cread_ptr + 1 - if CredentialDetails.get("snmpV2cWrite"): - snmpV2cWrite = CredentialDetails.get("snmpV2cWrite") + if CredentialDetails.get("snmp_v2c_write"): + snmpV2cWrite = CredentialDetails.get("snmp_v2c_write") have_snmpv2cwrite_ptr = 0 create_snmpv2cwrite_ptr = 0 update_snmpv2cwrite_ptr = 0 - values = ["writeCommunity", "description", "id"] + values = ["write_community", "description", "id"] + keys = ["writeCommunity", "description", "id"] have_snmpV2cWrite = self.have.get("globalCredential").get("snmpV2cWrite") for item in snmpV2cWrite: if not have_snmpV2cWrite or have_snmpV2cWrite[have_snmpv2cwrite_ptr] is None: @@ -1557,7 +1560,7 @@ def get_want_device_credentials(self, CredentialDetails): for i in range(0, 2): if item.get(values[i]): create_credential[create_snmpv2cwrite_ptr] \ - .update({values[i]: item.get(values[i])}) + .update({keys[i]: item.get(values[i])}) else: self.msg = values[i] + " is mandatory for creating \ snmpV2cWrite " + str(have_snmpv2cwrite_ptr) @@ -1569,11 +1572,11 @@ def get_want_device_credentials(self, CredentialDetails): want.get("want_update").update({"snmpV2cWrite": []}) update_credential = want.get("want_update").get("snmpV2cWrite") update_credential.append({}) - if item.get("writeCommunity"): + if item.get("write_community"): update_credential[update_snmpv2cwrite_ptr] \ - .update({"writeCommunity": item.get("writeCommunity")}) + .update({"writeCommunity": item.get("write_community")}) else: - self.msg = "writeCommunity is mandatory for updating \ + self.msg = "write_community is mandatory for updating \ snmpV2cWrite " + str(have_snmpv2cwrite_ptr) self.status = "failed" return self @@ -1589,8 +1592,8 @@ def get_want_device_credentials(self, CredentialDetails): update_snmpv2cwrite_ptr = update_snmpv2cwrite_ptr + 1 have_snmpv2cwrite_ptr = have_snmpv2cwrite_ptr + 1 - if CredentialDetails.get("httpsRead"): - httpsRead = CredentialDetails.get("httpsRead") + if CredentialDetails.get("https_read"): + httpsRead = CredentialDetails.get("https_read") have_httpsread_ptr = 0 create_httpsread_ptr = 0 update_httpsread_ptr = 0 @@ -1644,8 +1647,8 @@ def get_want_device_credentials(self, CredentialDetails): update_httpsread_ptr = update_httpsread_ptr + 1 have_httpsread_ptr = have_httpsread_ptr + 1 - if CredentialDetails.get("httpsWrite"): - httpsWrite = CredentialDetails.get("httpsWrite") + if CredentialDetails.get("https_write"): + httpsWrite = CredentialDetails.get("https_write") have_httpswrite_ptr = 0 create_httpswrite_ptr = 0 update_httpswrite_ptr = 0 @@ -1698,8 +1701,8 @@ def get_want_device_credentials(self, CredentialDetails): update_httpswrite_ptr = update_httpswrite_ptr + 1 have_httpswrite_ptr = have_httpswrite_ptr + 1 - if CredentialDetails.get("snmpV3"): - snmpV3 = CredentialDetails.get("snmpV3") + if CredentialDetails.get("snmp_v3"): + snmpV3 = CredentialDetails.get("snmp_v3") have_snmpv3_ptr = 0 create_snmpv3_ptr = 0 update_snmpv3_ptr = 0 @@ -1720,45 +1723,54 @@ def get_want_device_credentials(self, CredentialDetails): snmpV3 " + str(have_snmpv3_ptr) self.status = "failed" return self - if item.get("snmpMode"): + if item.get("snmp_mode"): create_credential[create_snmpv3_ptr] \ - .update({"snmpMode": item.get("snmpMode")}) + .update({"snmpMode": item.get("snmp_mode")}) else: create_credential[create_snmpv3_ptr] \ .update({"snmpMode": "AUTHPRIV"}) if create_credential[create_snmpv3_ptr].get("snmpMode") == "AUTHNOPRIV" or \ create_credential[create_snmpv3_ptr].get("snmpMode") == "AUTHPRIV": - auths = ["authPassword", "authType"] + auths = ["auth_password", "auth_type"] + keys = { + "auth_password": "authPassword", + "auth_type": "authType" + } for auth in auths: if item.get(auth): create_credential[create_snmpv3_ptr] \ - .update({auth: item.get(auth)}) + .update({keys[auth]: item.get(auth)}) else: self.msg = auth + " is mandatory for creating \ snmpV3 " + str(have_snmpv3_ptr) self.status = "failed" return self - if len(item.get("authPassword")) < 8: - self.msg = "authPassword length should be greater than 8" + if len(item.get("auth_password")) < 8: + self.msg = "auth_password length should be greater than 8" self.status = "failed" return self - elif create_credential[create_snmpv3_ptr].get("snmpMode") == "AUTHPRIV": - privs = ["privacyPassword", "privacyType"] + self.log(str(create_credential[create_snmpv3_ptr].get("snmpMode"))) + if create_credential[create_snmpv3_ptr].get("snmpMode") == "AUTHPRIV": + privs = ["privacy_password", "privacy_type"] + key = { + "privacy_password": "privacyPassword", + "privacy_type": "privacyType" + } for priv in privs: if item.get(priv): create_credential[create_snmpv3_ptr] \ - .update({priv: item.get(priv)}) + .update({key[priv]: item.get(priv)}) else: self.msg = priv + " is mandatory for creating \ snmpV3 " + str(have_snmpv3_ptr) self.status = "failed" return self - if len(item.get("privacyPassword")): - self.msg = "privacyPassword should be greater than 8" + if len(item.get("privacy_password")) < 8: + self.msg = "privacy_password should be greater than 8" self.status = "failed" return self elif create_credential[create_snmpv3_ptr].get("snmpMode") != "NOAUTHNOPRIV": - self.msg = "snmpMode in snmpV3 is not \ + self.msg = "snmp_mode in snmpV3 is not \ ['AUTHPRIV', 'AUTHNOPRIV', 'NOAUTHNOPRIV']" self.status = "failed" return self @@ -1777,14 +1789,14 @@ def get_want_device_credentials(self, CredentialDetails): value: self.have.get("globalCredential") .get("snmpV3")[have_snmpv3_ptr].get(value) }) - if item.get("snmpMode"): + if item.get("snmp_mode"): update_credential[update_snmpv3_ptr] \ - .update({"snmpMode": item.get("snmpMode")}) + .update({"snmpMode": item.get("snmp_mode")}) if update_credential[update_snmpv3_ptr].get("snmpMode") == "AUTHNOPRIV" or \ update_credential[update_snmpv3_ptr].get("snmpMode") == "AUTHPRIV": - if item.get("authType"): + if item.get("auth_type"): update_credential[update_snmpv3_ptr] \ - .update({"authType": item.get("authType")}) + .update({"authType": item.get("auth_type")}) elif self.have.get("globalCredential") \ .get("snmpMode")[have_snmpv3_ptr].get("authType"): update_credential[update_snmpv3_ptr].update({ @@ -1792,26 +1804,26 @@ def get_want_device_credentials(self, CredentialDetails): .get("snmpMode")[have_snmpv3_ptr].get("authType") }) else: - self.msg = "authType is required for updating snmpV3 " + \ + self.msg = "auth_type is required for updating snmpV3 " + \ str(have_snmpv3_ptr) self.status = "failed" return self - if item.get("authPassword"): + if item.get("auth_password"): update_credential[update_snmpv3_ptr] \ - .update({"authPassword": item.get("authPassword")}) + .update({"authPassword": item.get("auth_password")}) else: - self.msg = "authPassword is required for updating snmpV3 " + \ + self.msg = "auth_password is required for updating snmpV3 " + \ str(have_snmpv3_ptr) self.status = "failed" return self - if len(item.get("authPassword")) < 8: - self.msg = "authPassword length should be greater than 8" + if len(item.get("auth_password")) < 8: + self.msg = "auth_password length should be greater than 8" self.status = "failed" return self elif update_credential[update_snmpv3_ptr].get("snmpMode") == "AUTHPRIV": - if item.get("privacyType"): + if item.get("privacy_type"): update_credential[update_snmpv3_ptr] \ - .update({"privacyType": item.get("privacyType")}) + .update({"privacyType": item.get("privacy_type")}) elif self.have.get("globalCredential") \ .get("snmpMode")[have_snmpv3_ptr].get("privacyType"): update_credential[update_snmpv3_ptr].update({ @@ -1819,20 +1831,20 @@ def get_want_device_credentials(self, CredentialDetails): .get("snmpMode")[have_snmpv3_ptr].get("privacyType") }) else: - self.msg = "privacyType is required for updating snmpV3 " + \ + self.msg = "privacy_type is required for updating snmpV3 " + \ str(have_snmpv3_ptr) self.status = "failed" return self - if item.get("privacyPassword"): + if item.get("privacy_password"): update_credential[update_snmpv3_ptr] \ - .update({"privacyPassword": item.get("privacyPassword")}) + .update({"privacyPassword": item.get("privacy_password")}) else: - self.msg = "privacyPassword is required for updating snmpV3 " + \ + self.msg = "privacy_password is required for updating snmpV3 " + \ str(have_snmpv3_ptr) self.status = "failed" return self - if len(item.get("privacyPassword")) < 8: - self.msg = "privacyPassword length should be greater than 8" + if len(item.get("privacy_password")) < 8: + self.msg = "privacy_password length should be greater than 8" self.status = "failed" return self update_snmpv3_ptr = update_snmpv3_ptr + 1 @@ -1858,24 +1870,24 @@ def get_want_assign_credentials(self, AssignCredentials): want = { "assign_credentials": {} } - siteName = AssignCredentials.get("siteName") - if not siteName: - self.msg = "siteName is required for AssignCredentials" + site_name = AssignCredentials.get("site_name") + if not site_name: + self.msg = "site_name is required for AssignCredentials" self.status = "failed" return self site_id = [] - for site_name in siteName: + for site_name in site_name: siteId = self.get_site_id(site_name) if not site_name: - self.msg = "siteName is invalid in AssignCredentials" + self.msg = "site_name is invalid in AssignCredentials" self.status = "failed" return self site_id.append(siteId) want.update({"site_id": site_id}) global_credentials = self.get_global_credentials_params() - cliId = AssignCredentials.get("cliId") - cliDescription = AssignCredentials.get("cliDescription") - cliUsername = AssignCredentials.get("cliUsername") + cliId = AssignCredentials.get("cli_id") + cliDescription = AssignCredentials.get("cli_description") + cliUsername = AssignCredentials.get("cli_username") if cliId or cliDescription and cliUsername: # All CLI details from the Cisco DNA Center @@ -1902,8 +1914,8 @@ def get_want_assign_credentials(self, AssignCredentials): return self want.get("assign_credentials").update({"cliId": cliDetail.get("id")}) - snmpV2cReadId = AssignCredentials.get("snmpV2ReadId") - snmpV2cReadDescription = AssignCredentials.get("snmpV2ReadDescription") + snmpV2cReadId = AssignCredentials.get("snmp_v2_read_id") + snmpV2cReadDescription = AssignCredentials.get("snmp_v2_read_description") if snmpV2cReadId or snmpV2cReadDescription: # All snmpV2cRead details from the Cisco DNA Center @@ -1929,8 +1941,8 @@ def get_want_assign_credentials(self, AssignCredentials): return self want.get("assign_credentials").update({"snmpV2ReadId": snmpV2cReadDetail.get("id")}) - snmpV2cWriteId = AssignCredentials.get("snmpV2WriteId") - snmpV2cWriteDescription = AssignCredentials.get("snmpV2WriteDescription") + snmpV2cWriteId = AssignCredentials.get("snmp_v2_write_id") + snmpV2cWriteDescription = AssignCredentials.get("snmp_v2_write_description") if snmpV2cWriteId or snmpV2cWriteDescription: # All snmpV2cWrite details from the Cisco DNA Center @@ -1956,9 +1968,9 @@ def get_want_assign_credentials(self, AssignCredentials): return self want.get("assign_credentials").update({"snmpV2WriteId": snmpV2cWriteDetail.get("id")}) - httpReadId = AssignCredentials.get("httpRead") - httpReadDescription = AssignCredentials.get("httpReadDescription") - httpReadUsername = AssignCredentials.get("httpReadUsername") + httpReadId = AssignCredentials.get("http_read") + httpReadDescription = AssignCredentials.get("http_read_description") + httpReadUsername = AssignCredentials.get("http_read_username") if httpReadId or httpReadDescription and httpReadUsername: # All httpRead details from the Cisco DNA Center @@ -1985,9 +1997,9 @@ def get_want_assign_credentials(self, AssignCredentials): return self want.get("assign_credentials").update({"httpRead": httpReadDetail.get("id")}) - httpWriteId = AssignCredentials.get("httpWrite") - httpWriteDescription = AssignCredentials.get("httpWriteDescription") - httpWriteUsername = AssignCredentials.get("httpWriteUsername") + httpWriteId = AssignCredentials.get("http_write") + httpWriteDescription = AssignCredentials.get("http_write_description") + httpWriteUsername = AssignCredentials.get("http_write_username") if httpWriteId or httpWriteDescription and httpWriteUsername: # All httpWrite details from the Cisco DNA Center @@ -2014,8 +2026,8 @@ def get_want_assign_credentials(self, AssignCredentials): return self want.get("assign_credentials").update({"httpWrite": httpWriteDetail.get("id")}) - snmpV3Id = AssignCredentials.get("snmpV3Id") - snmpV3Description = AssignCredentials.get("snmpV3Description") + snmpV3Id = AssignCredentials.get("snmp_v3_id") + snmpV3Description = AssignCredentials.get("snmp_v3_description") if snmpV3Id or snmpV3Description: # All snmpV3 details from the Cisco DNA Center @@ -2061,12 +2073,12 @@ def get_want(self, config): Device Credentials and Device Credentials assigned to a site. """ - if config.get("GlobalCredentialDetails"): - CredentialDetails = config.get("GlobalCredentialDetails") + if config.get("global_credential_details"): + CredentialDetails = config.get("global_credential_details") self.get_want_device_credentials(CredentialDetails).check_return_status() - if config.get("AssignCredentialsToSite"): - AssignCredentials = config.get("AssignCredentialsToSite") + if config.get("assign_credentials_to_site"): + AssignCredentials = config.get("assign_credentials_to_site") self.get_want_assign_credentials(AssignCredentials).check_return_status() self.log("User details from the playbook " + str(self.want)) @@ -2213,7 +2225,7 @@ def assign_credentials_to_site(self): site_ids = self.want.get("site_id") for site_id in site_ids: credential_params.update({"site_id": site_id}) - final_response.append(credential_params) + final_response.append(copy.deepcopy(credential_params)) response = self.dnac._exec( family="network_settings", function='assign_device_credential_to_site_v2', @@ -2223,6 +2235,7 @@ def assign_credentials_to_site(self): validation_string = "desired common settings operation successful" self.check_task_response_status(response, validation_string).check_return_status() self.log("Device Credential Assigned to site is Successfully") + self.log(str(final_response)) result_assign_credential.update({ "Assign Credentials": { "response": final_response, @@ -2246,13 +2259,13 @@ def get_diff_merged(self, config): self """ - if config.get("GlobalCredentialDetails") is not None: + if config.get("global_credential_details") is not None: self.create_device_credentials().check_return_status() - if config.get("GlobalCredentialDetails") is not None: + if config.get("global_credential_details") is not None: self.update_device_credentials().check_return_status() - if config.get("AssignCredentialsToSite") is not None: + if config.get("assign_credentials_to_site") is not None: self.assign_credentials_to_site().check_return_status() return self @@ -2279,10 +2292,10 @@ def delete_device_credential(self, config): final_response.update({item: []}) for value in have_values.get(item): if value is None: - self.msg = str(config.get("GlobalCredentialDetails") - .get("item")[config_itr]) + "is not found" - self.status = "failed" - return self + final_response.get(item).append( + str(config.get("global_credential_details").get(item)[config_itr]) + " is not found." + ) + continue _id = have_values.get(item)[config_itr].get("id") response = self.dnac._exec( family="discovery", @@ -2319,7 +2332,7 @@ def get_diff_deleted(self, config): self """ - if config.get("GlobalCredentialDetails") is not None: + if config.get("global_credential_details") is not None: self.delete_device_credential(config).check_return_status() return self diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index 256c9c1b93..4286ccb72d 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -37,7 +37,7 @@ elements: dict required: true suboptions: - GlobalPoolDetails: + global_pool_details: description: Global ip pool manages IPv4 and IPv6 IP pools. type: dict suboptions: @@ -50,30 +50,30 @@ elements: dict type: list suboptions: - dhcpServerIps: + dhcp_server_ips: description: Dhcp Server Ips. elements: str type: list - dnsServerIps: + dns_server_ips: description: Dns Server Ips. elements: str type: list gateway: description: Gateway. type: str - IpAddressSpace: + ip_address_space: description: Ip address space. type: str - ipPoolCidr: + ip_pool_cidr: description: Ip pool cidr. type: str prev_name: description: previous name. type: str - ipPoolName: + ip_pool_name: description: Ip Pool Name. type: str - ReservePoolDetails: + reserve_pool_details: description: Reserving IP subpool from the global pool type: dict suboptions: @@ -81,7 +81,7 @@ description: IPv4 input for dhcp server ip example 1.1.1.1. elements: str type: list - ipv4DnsServers: + ipv4_dns_servers: description: IPv4 input for dns server ip example 4.4.4.4. elements: str type: list @@ -89,22 +89,22 @@ description: Gateway ip address details, example 175.175.0.1. type: str version_added: 4.0.0 - ipv4GlobalPool: + ipv4_global_pool: description: IP v4 Global pool address with cidr, example 175.175.0.0/16. type: str - ipv4Prefix: + ipv4_prefix: description: ip4 prefix length is enabled or ipv4 total Host input is enabled type: bool - ipv4PrefixLength: - description: The ipv4 prefix length is required when ipv4prefix value is true. + ipv4_prefix_length: + description: The ipv4 prefix length is required when ipv4_prefix value is true. type: int - ipv4Subnet: + ipv4_subnet: description: IPv4 Subnet address, example 175.175.0.0. type: str ipv4TotalHost: - description: IPv4 total host is required when ipv4prefix value is false. + description: IPv4 total host is required when ipv4_prefix value is false. type: int - ipv6AddressSpace: + ipv6_address_space: description: > If the value is false only ipv4 input are required, otherwise both ipv6 and ipv4 are required. @@ -120,24 +120,24 @@ ipv6GateWay: description: Gateway ip address details, example 2001 db8 85a3 0 100 1. type: str - ipv6GlobalPool: + ipv6_global_pool: description: > - IPv6 Global pool address with cidr this is required when Ipv6AddressSpace + IPv6 Global pool address with cidr this is required when ipv6_address_space value is true, example 2001 db8 85a3 /64. type: str - ipv6Prefix: + 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. type: bool - ipv6PrefixLength: - description: IPv6 prefix length is required when the ipv6prefix value is true. + ipv6_prefix_length: + description: IPv6 prefix length is required when the ipv6_prefix value is true. type: int - ipv6Subnet: + ipv6_subnet: description: IPv6 Subnet address, example 2001 db8 85a3 0 100. type: str ipv6TotalHost: - description: IPv6 total host is required when ipv6prefix value is false. + description: IPv6 total host is required when ipv6_prefix value is false. type: int name: description: Name of the reserve ip sub pool. @@ -145,16 +145,16 @@ prev_name: description: Previous name of the reserve ip sub pool. type: str - siteName: + site_name: description: Site name path parameter. Site name to reserve the ip sub pool. type: str - slaacSupport: + slaac_support: description: Slaac Support. type: bool type: description: Type of the reserve ip sub pool. type: str - NetworkManagementDetails: + network_management_details: description: Set default network settings for the site type: dict suboptions: @@ -162,10 +162,10 @@ description: Network management details settings. type: dict suboptions: - clientAndEndpoint_aaa: + client_and_endpoint_aaa: description: Network V2's clientAndEndpoint_aaa. suboptions: - ipAddress: + ip_address: description: IP address for ISE serve (eg 1.1.1.4). type: str network: @@ -177,41 +177,41 @@ servers: description: Server type AAA or ISE server (eg AAA). type: str - sharedSecret: + shared_secret: description: Shared secret for ISE server. type: str type: dict - dhcpServer: + dhcp_server: description: DHCP Server IP (eg 1.1.1.1). elements: str type: list - dnsServer: + dns_server: description: Network V2's dnsServer. suboptions: - domainName: + domain_name: description: Domain Name of DHCP (eg; cisco). type: str - primaryIpAddress: + primary_ip_address: description: Primary IP Address for DHCP (eg 2.2.2.2). type: str - secondaryIpAddress: + secondary_ip_address: description: Secondary IP Address for DHCP (eg 3.3.3.3). type: str type: dict - messageOfTheday: + message_of_the_day: description: Network V2's messageOfTheday. suboptions: - bannerMessage: + banner_message: description: Massage for Banner message (eg; Good day). type: str - retainExistingBanner: + retain_existing_banner: description: Retain existing Banner Message (eg "true" or "false"). type: str type: dict - netflowcollector: + netflow_collector: description: Network V2's netflowcollector. suboptions: - ipAddress: + ip_address: description: IP Address for NetFlow collector (eg 3.3.3.1). type: str port: @@ -221,7 +221,7 @@ network_aaa: description: Network V2's network_aaa. suboptions: - ipAddress: + ip_address: description: IP address for AAA and ISE server (eg 1.1.1.1). type: str network: @@ -233,21 +233,21 @@ servers: description: Server type for AAA Network (eg AAA). type: str - sharedSecret: + shared_secret: description: Shared secret for ISE Server. type: str type: dict - ntpServer: + ntp_server: description: IP address for NTP server (eg 1.1.1.2). elements: str type: list - snmpServer: + snmp_server: description: Network V2's snmpServer. suboptions: - configureDnacIP: + configure_dnac_ip: description: Configuration Cisco DNA Center IP for SNMP Server (eg true). type: bool - ipAddresses: + ip_addresses: description: IP Address for SNMP Server (eg 4.4.4.1). elements: str type: list @@ -255,10 +255,10 @@ syslogServer: description: Network V2's syslogServer. suboptions: - configureDnacIP: + configure_dnac_ip: description: Configuration Cisco DNA Center IP for syslog server (eg true). type: bool - ipAddresses: + ip_addresses: description: IP Address for syslog server (eg 4.4.4.4). elements: str type: list @@ -266,7 +266,7 @@ timezone: description: Input for time zone (eg Africa/Abidjan). type: str - siteName: + site_name: description: Site name path parameter. type: str requirements: @@ -306,59 +306,59 @@ dnac_log: True state: merged config: - - GlobalPoolDetails: + - global_pool_details: settings: ippool: - - ipPoolName: string + - ip_pool_name: string gateway: string - IpAddressSpace: string - ipPoolCidr: string + ip_address_space: string + ip_pool_cidr: string type: Generic - dhcpServerIps: list - dnsServerIps: list - ReservePoolDetails: - ipv6AddressSpace: True - ipv4GlobalPool: string - ipv4Prefix: True - ipv4PrefixLength: 9 - ipv4Subnet: string + dhcp_server_ips: list + dns_server_ips: list + reserve_pool_details: + ipv6_address_space: True + ipv4_global_pool: string + ipv4_prefix: True + ipv4_prefix_length: 9 + ipv4_subnet: string name: string - ipv6Prefix: True - ipv6PrefixLength: 64 - ipv6GlobalPool: string - ipv6Subnet: string - siteName: string - slaacSupport: True + ipv6_prefix: True + ipv6_prefix_length: 64 + ipv6_global_pool: string + ipv6_subnet: string + site_name: string + slaac_support: True type: LAN - NetworkManagementDetails: + network_management_details: settings: - dhcpServer: list - dnsServer: - domainName: string - primaryIpAddress: string - secondaryIpAddress: string - clientAndEndpoint_aaa: + dhcp_server: list + dns_server: + domain_name: string + primary_ip_address: string + secondary_ip_address: string + client_and_endpoint_aaa: network: string protocol: string servers: string - messageOfTheday: - bannerMessage: string - retainExistingBanner: string - netflowcollector: - ipAddress: string + message_of_the_day: + banner_message: string + retain_existing_banner: string + netflow_collector: + ip_address: string port: 443 network_aaa: network: string protocol: string servers: string - ntpServer: list - snmpServer: - configureDnacIP: True - ipAddresses: list + ntp_server: list + snmp_server: + configure_dnac_ip: True + ip_addresses: list syslogServer: - configureDnacIP: True - ipAddresses: list - siteName: string + configure_dnac_ip: True + ip_addresses: list + site_name: string """ RETURN = r""" @@ -425,12 +425,13 @@ def validate_input(self): Checks if the configuration parameters provided in the playbook meet the expected structure and data types, as defined in the 'temp_spec' dictionary. + Parameters: - - self: The instance of the class containing the 'config' attribute + None + Returns: - The method returns an instance of the class with updated attributes: - - self.msg: A message describing the validation result. - - self.status: The status of the validation (either 'success' or 'failed'). + self + """ if not self.config: @@ -440,98 +441,98 @@ def validate_input(self): # temp_spec is the specification for the expected structure of configuration parameters temp_spec = { - "GlobalPoolDetails": { + "global_pool_details": { "type": 'dict', "settings": { "type": 'dict', "ippool": { "type": 'list', - "IpAddressSpace": {"type": 'string'}, - "dhcpServerIps": {"type": 'list'}, - "dnsServerIps": {"type": 'list'}, + "ip_address_space": {"type": 'string'}, + "dhcp_server_ips": {"type": 'list'}, + "dns_server_ips": {"type": 'list'}, "gateway": {"type": 'string'}, - "ipPoolCidr": {"type": 'string'}, - "ipPoolName": {"type": 'string'}, + "ip_pool_cidr": {"type": 'string'}, + "ip_pool_name": {"type": 'string'}, "prevName": {"type": 'string'}, } } }, - "ReservePoolDetails": { + "reserve_pool_details": { "type": 'dict', "name": {"type": 'string'}, "prevName": {"type": 'string'}, - "ipv6AddressSpace": {"type": 'bool'}, - "ipv4GlobalPool": {"type": 'string'}, - "ipv4Prefix": {"type": 'bool'}, - "ipv4PrefixLength": {"type": 'string'}, - "ipv4Subnet": {"type": 'string'}, + "ipv6_address_space": {"type": 'bool'}, + "ipv4_global_pool": {"type": 'string'}, + "ipv4_prefix": {"type": 'bool'}, + "ipv4_prefix_length": {"type": 'string'}, + "ipv4_subnet": {"type": 'string'}, "ipv4GateWay": {"type": 'string'}, "ipv4DhcpServers": {"type": 'list'}, - "ipv4DnsServers": {"type": 'list'}, - "ipv6GlobalPool": {"type": 'string'}, - "ipv6Prefix": {"type": 'bool'}, - "ipv6PrefixLength": {"type": 'integer'}, - "ipv6Subnet": {"type": 'string'}, + "ipv4_dns_servers": {"type": 'list'}, + "ipv6_global_pool": {"type": 'string'}, + "ipv6_prefix": {"type": 'bool'}, + "ipv6_prefix_length": {"type": 'integer'}, + "ipv6_subnet": {"type": 'string'}, "ipv6GateWay": {"type": 'string'}, "ipv6DhcpServers": {"type": 'list'}, "ipv6DnsServers": {"type": 'list'}, "ipv4TotalHost": {"type": 'integer'}, "ipv6TotalHost": {"type": 'integer'}, - "slaacSupport": {"type": 'bool'}, - "siteName": {"type": 'string'}, + "slaac_support": {"type": 'bool'}, + "site_name": {"type": 'string'}, }, - "NetworkManagementDetails": { + "network_management_details": { "type": 'dict', "settings": { "type": 'dict', - "dhcpServer": {"type": 'list'}, - "dnsServer": { + "dhcp_server": {"type": 'list'}, + "dns_server": { "type": 'dict', - "domainName": {"type": 'string'}, - "primaryIpAddress": {"type": 'string'}, - "secondaryIpAddress": {"type": 'string'} + "domain_name": {"type": 'string'}, + "primary_ip_address": {"type": 'string'}, + "secondary_ip_address": {"type": 'string'} }, "syslogServer": { "type": 'dict', - "ipAddresses": {"type": 'list'}, - "configureDnacIP": {"type": 'bool'} + "ip_addresses": {"type": 'list'}, + "configure_dnac_ip": {"type": 'bool'} }, - "snmpServer": { + "snmp_server": { "type": 'dict', - "ipAddresses": {"type": 'list'}, - "configureDnacIP": {"type": 'bool'} + "ip_addresses": {"type": 'list'}, + "configure_dnac_ip": {"type": 'bool'} }, - "netflowcollector": { + "netflow_collector": { "type": 'dict', - "ipAddress": {"type": 'string'}, + "ip_address": {"type": 'string'}, "port": {"type": 'integer'}, }, "timezone": {"type": 'string'}, - "ntpServer": {"type": 'list'}, - "messageOfTheday": { + "ntp_server": {"type": 'list'}, + "message_of_the_day": { "type": 'dict', - "bannerMessage": {"type": 'string'}, - "retainExistingBanner": {"type": 'bool'}, + "banner_message": {"type": 'string'}, + "retain_existing_banner": {"type": 'bool'}, }, "network_aaa": { "type": 'dict', "servers": {"type": 'string', "choices": ["ISE", "AAA"]}, - "ipAddress": {"type": 'string'}, + "ip_address": {"type": 'string'}, "network": {"type": 'string'}, "protocol": {"type": 'string', "choices": ["RADIUS", "TACACS"]}, - "sharedSecret": {"type": 'string'} + "shared_secret": {"type": 'string'} }, - "clientAndEndpoint_aaa": { + "client_and_endpoint_aaa": { "type": 'dict', "servers": {"type": 'string', "choices": ["ISE", "AAA"]}, - "ipAddress": {"type": 'string'}, + "ip_address": {"type": 'string'}, "network": {"type": 'string'}, "protocol": {"type": 'string', "choices": ["RADIUS", "TACACS"]}, - "sharedSecret": {"type": 'string'} + "shared_secret": {"type": 'string'} } }, - "siteName": {"type": 'string'}, + "site_name": {"type": 'string'}, } } @@ -558,18 +559,18 @@ def requires_update(self, have, want, obj_params): from the playbook, using a specified schema for comparison. Parameters: - - self: The instance of the class containing the 'config' attribute - - have (dict): Current information from the Cisco DNA Center + have (dict) - Current information from the Cisco DNA Center (global pool, reserve pool, network details) - - want (dict): Users provided information from the playbook - - obj_params (list of tuples): A list of parameter mappings - specifying which Cisco DNA Center parameters (dnac_param) - correspond to the user-provided parameters (ansible_param). + want (dict) - Users provided information from the playbook + obj_params (list of tuples) - A list of parameter mappings specifying which + Cisco DNA Center parameters (dnac_param) correspond to + the user-provided parameters (ansible_param). + Returns: - - bool: True if any parameter specified in obj_params differs - between current_obj and requested_obj, indicating that an - update is required. - False if all specified parameters are equal. + bool - True if any parameter specified in obj_params differs between + current_obj and requested_obj, indicating that an update is required. + False if all specified parameters are equal. + """ current_obj = have @@ -584,12 +585,13 @@ def requires_update(self, have, want, obj_params): def get_site_id(self, site_name): """ Get the site id from the site name. - Use check_return_status() to check for failure. + Use check_return_status() to check for failure Parameters: - - site_name (str): Site name + site_name (str) - Site name + Returns: - - str or None - The Site Id if found, or None if not found or error + str or None - The Site Id if found, or None if not found or error """ try: @@ -613,16 +615,14 @@ def get_site_id(self, site_name): def get_global_pool_params(self, pool_info): """ - Process Global Pool params from playbook data for Global Pool config - in Cisco DNA Center + Process Global Pool params from playbook data for Global Pool config in Cisco DNA Center Parameters: - - pool_info (dict): Playbook data containing information about - the global pool + pool_info (dict) - Playbook data containing information about the global pool + Returns: - - dict or None: Processed Global Pool data in a format suitable - for Cisco DNA Center configuration, or None - if pool_info is empty. + dict or None - Processed Global Pool data in a format suitable + for Cisco DNA Center configuration, or None if pool_info is empty. """ if not pool_info: @@ -659,15 +659,14 @@ def get_global_pool_params(self, pool_info): def get_reserve_pool_params(self, pool_info): """ Process Reserved Pool parameters from playbook data - for Reserved Pool configuration in Cisco DNA Center. + for Reserved Pool configuration in Cisco DNA Center Parameters: - - pool_info (dict): Playbook data containing information about - the reserved pool. + pool_info (dict) - Playbook data containing information about the reserved pool Returns: - - reserve_pool (dict): Processed Reserved pool data in the format - suitable for the Cisco DNA Center config. + reserve_pool (dict) - Processed Reserved pool data + in the format suitable for the Cisco DNA Center config """ reserve_pool = { @@ -737,18 +736,17 @@ def get_network_params(self, site_id): for Network configuration in Cisco DNA Center Parameters: - - site_id (str): The Site ID for which network parameters - are requested. + site_id (str) - The Site ID for which network parameters are requested Returns: - - dict or None: Processed Network data in a format suitable for - Cisco DNA Center configuration, or None if the - response is not a dictionary or there was an error. + dict or None: Processed Network data in a format + suitable for Cisco DNA Center configuration, or None + if the response is not a dictionary or there was an error. """ response = self.dnac._exec( family="network_settings", - function='get_network', + function='get_network_v2', params={"site_id": site_id} ) self.log(str(response)) @@ -765,10 +763,12 @@ def get_network_params(self, site_id): netflow_details = get_dict_result(all_network_details, "key", "netflow.collector") ntpserver_details = get_dict_result(all_network_details, "key", "ntp.server") timezone_details = get_dict_result(all_network_details, "key", "timezone.site") - messageoftheday_details = get_dict_result(all_network_details, "key", "banner.setting") + messageoftheday_details = get_dict_result(all_network_details, "key", "device.banner") network_aaa = get_dict_result(all_network_details, "key", "aaa.network.server.1") + network_aaa2 = get_dict_result(all_network_details, "key", "aaa.network.server.2") network_aaa_pan = get_dict_result(all_network_details, "key", "aaa.server.pan.network") clientAndEndpoint_aaa = get_dict_result(all_network_details, "key", "aaa.endpoint.server.1") + clientAndEndpoint_aaa2 = get_dict_result(all_network_details, "key", "aaa.endpoint.server.2") clientAndEndpoint_aaa_pan = \ get_dict_result(all_network_details, "key", "aaa.server.pan.endpoint") @@ -785,15 +785,16 @@ def get_network_params(self, site_id): }, "netflowcollector": { "ipAddress": netflow_details.get("value")[0].get("ipAddress"), - "port": netflow_details.get("value")[0].get("port"), - "configureDnacIP": netflow_details.get("value")[0].get("configureDnacIP"), + "port": netflow_details.get("value")[0].get("port") }, "timezone": timezone_details.get("value")[0], } } network_settings = network_details.get("settings") - if dhcp_details is not None: + if dhcp_details.get("value") != []: network_settings.update({"dhcpServer": dhcp_details.get("value")}) + else: + network_settings.update({"dhcpServer": [""]}) if dns_details is not None: network_settings.update({ @@ -804,35 +805,66 @@ def get_network_params(self, site_id): } }) - if ntpserver_details is not None: + if ntpserver_details.get("value") != []: network_settings.update({"ntpServer": ntpserver_details.get("value")}) + else: + network_settings.update({"ntpServer": [""]}) if messageoftheday_details is not None: network_settings.update({ "messageOfTheday": { "bannerMessage": messageoftheday_details.get("value")[0].get("bannerMessage"), - "retainExistingBanner": - messageoftheday_details.get("value")[0].get("retainExistingBanner"), } }) + if messageoftheday_details.get("value")[0].get("retainExistingBanner") is True: + network_settings.get("messageOfTheday").update({ + "retainExistingBanner": "true" + }) + else: + network_settings.get("messageOfTheday").update({ + "retainExistingBanner": "false" + }) if network_aaa and network_aaa_pan: - network_settings.update({ - "network_aaa": { - "network": network_aaa.get("value")[0].get("ipAddress"), - "protocol": network_aaa.get("value")[0].get("protocol"), - "ipAddress": network_aaa_pan.get("value")[0] - } - }) + if network_aaa_pan.get("value")[0] == "None": + network_settings.update({ + "network_aaa": { + "network": network_aaa.get("value")[0].get("ipAddress"), + "protocol": network_aaa.get("value")[0].get("protocol"), + "ipAddress": network_aaa2.get("value")[0].get("ipAddress"), + "servers": "AAA" + } + }) + else: + network_settings.update({ + "network_aaa": { + "network": network_aaa.get("value")[0].get("ipAddress"), + "protocol": network_aaa.get("value")[0].get("protocol"), + "ipAddress": network_aaa_pan.get("value")[0], + "servers": "ISE" + } + }) if clientAndEndpoint_aaa and clientAndEndpoint_aaa_pan: - network_settings.update({ - "clientAndEndpoint_aaa": { - "network": clientAndEndpoint_aaa.get("value")[0].get("ipAddress"), - "protocol": clientAndEndpoint_aaa.get("value")[0].get("protocol"), - "ipAddress": clientAndEndpoint_aaa_pan.get("value")[0], - } - }) + if clientAndEndpoint_aaa_pan.get("value")[0] == "None": + network_settings.update({ + "clientAndEndpoint_aaa": { + "network": clientAndEndpoint_aaa.get("value")[0].get("ipAddress"), + "protocol": clientAndEndpoint_aaa.get("value")[0].get("protocol"), + "ipAddress": clientAndEndpoint_aaa2.get("value")[0].get("ipAddress"), + "servers": "AAA" + } + }) + else: + network_settings.update({ + "clientAndEndpoint_aaa": { + "network": clientAndEndpoint_aaa.get("value")[0].get("ipAddress"), + "protocol": clientAndEndpoint_aaa.get("value")[0].get("protocol"), + "ipAddress": clientAndEndpoint_aaa_pan.get("value")[0], + "servers": "ISE" + } + }) + self.log(str(network_details)) return network_details @@ -841,13 +873,13 @@ def global_pool_exists(self, name): Check if the Global Pool with the given name exists Parameters: - - name (str): The name of the Global Pool to check for existence + name (str) - The name of the Global Pool to check for existence + Returns: - dict: A dictionary containing information about the - Global Pool's existence: - - 'exists' (bool): True if Global Pool exists, otherwise False. - - 'id' (str): ID of the Global Pool if exists, otherwise None - - 'details' (dict): Details of Global Pool if exists, otherwise None. + dict - A dictionary containing information about the Global Pool's existence: + - 'exists' (bool): True if the Global Pool exists, False otherwise. + - 'id' (str or None): The ID of the Global Pool if it exists, or None if it doesn't. + - 'details' (dict or None): Details of the Global Pool if it exists, else None. """ global_pool = { @@ -883,18 +915,14 @@ def reserve_pool_exists(self, name, site_name): Use check_return_status() to check for failure Parameters: - - name (str): Name of the Reserved pool to check for existence. - - site_name (str): Name of the site where Reserved pool is located. + name (str) - The name of the Reserved pool to check for existence. + site_name (str) - The name of the site where the Reserved pool is located. Returns: - dict - Dictionary containing information about the - Reserved pool's existence: - - 'exists' (bool): True if the Reserved pool exists in the specified - site, otherwise False. - - 'id' (str or None): ID of the Reserved pool if it exists, or - None if it doesn't. - - 'details' (dict or None): Details of the Reserved pool if exists, - or else None. + dict - A dictionary containing information about the Reserved pool's existence: + - 'exists' (bool): True if the Reserved pool exists in the specified site, else False. + - 'id' (str or None): The ID of the Reserved pool if it exists, or None if it doesn't. + - 'details' (dict or None): Details of the Reserved pool if it exists, or else None. """ reserve_pool = { @@ -943,7 +971,8 @@ def get_have_global_pool(self, config): check this API using check_return_status. Parameters: - - config (dict): Playbook details containing Global Pool config. + config (dict) - Playbook details containing Global Pool configuration. + Returns: self - The current object with updated information. """ @@ -953,21 +982,21 @@ def get_have_global_pool(self, config): "details": None, "id": None } - global_pool_settings = config.get("GlobalPoolDetails").get("settings") + global_pool_settings = config.get("global_pool_details").get("settings") if global_pool_settings is None: - self.msg = "settings in GlobalPoolDetails is missing in the playbook" + self.msg = "settings in global_pool_details is missing in the playbook" self.status = "failed" return self global_pool_ippool = global_pool_settings.get("ippool") if global_pool_ippool is None: - self.msg = "ippool in GlobalPoolDetails is missing in the playbook" + self.msg = "ippool in global_pool_details is missing in the playbook" self.status = "failed" return self - name = global_pool_ippool[0].get("ipPoolName") + name = global_pool_ippool[0].get("ip_pool_name") if name is None: - self.msg = "Mandatory Parameter ipPoolName required" + self.msg = "Mandatory Parameter ip_pool_name required" self.status = "failed" return self @@ -980,7 +1009,7 @@ def get_have_global_pool(self, config): prev_name is not None: global_pool = self.global_pool_exists(prev_name) if global_pool.get("exists") is False: - self.msg = "Prev name {0} doesn't exist in GlobalPoolDetails".format(prev_name) + self.msg = "Prev name {0} doesn't exist in global_pool_details".format(prev_name) self.status = "failed" return self @@ -998,8 +1027,8 @@ def get_have_reserve_pool(self, config): Check this API using check_return_status Parameters: - - config (list of dict): Playbook details containing Reserved Pool - configuration. + config (list of dict) - Playbook details containing Reserved Pool configuration. + Returns: self - The current object with updated information. """ @@ -1009,17 +1038,17 @@ def get_have_reserve_pool(self, config): "details": None, "id": None } - reserve_pool_details = config.get("ReservePoolDetails") + reserve_pool_details = config.get("reserve_pool_details") name = reserve_pool_details.get("name") if name is None: - self.msg = "Mandatory Parameter name required in ReservePoolDetails\n" + self.msg = "Mandatory Parameter name required in reserve_pool_details\n" self.status = "failed" return self - site_name = reserve_pool_details.get("siteName") + site_name = reserve_pool_details.get("site_name") self.log(str(site_name)) if site_name is None: - self.msg = "Missing parameter 'siteName' in ReservePoolDetails" + self.msg = "Missing parameter 'site_name' in reserve_pool_details" self.status = "failed" return self @@ -1041,7 +1070,7 @@ def get_have_reserve_pool(self, config): # If the previous name doesn't exist in Cisco DNA Center, return with error if reserve_pool.get("exists") is False: - self.msg = "Prev name {0} doesn't exist in ReservePoolDetails".format(prev_name) + self.msg = "Prev name {0} doesn't exist in reserve_pool_details".format(prev_name) self.status = "failed" return self @@ -1068,15 +1097,15 @@ def get_have_network(self, config): Center based on the provided playbook details. Parameters: - config (dict) - Playbook details containing Network Management - configuration. + config (dict) - Playbook details containing Network Management configuration. + Returns: self - The current object with updated Network information. """ network = {} - site_name = config.get("NetworkManagementDetails").get("siteName") + site_name = config.get("network_management_details").get("site_name") if site_name is None: - self.msg = "Mandatory Parameter 'siteName' missing" + self.msg = "Mandatory Parameter 'site_name' missing" self.status = "failed" return self @@ -1096,25 +1125,24 @@ def get_have_network(self, config): def get_have(self, config): """ - Get the current Global Pool Reserved Pool and Network details - from Cisco DNA Center + Get the current Global Pool Reserved Pool and Network details from Cisco DNA Center Parameters: - - config (dict): Playbook details containing Global Pool, Reserved - Pool, and Network Management configuration. + config (dict) - Playbook details containing Global Pool, + Reserved Pool, and Network Management configuration. Returns: - - self: The current object with updated Global Pool, - Reserved Pool, and Network information. + self - The current object with updated Global Pool, + Reserved Pool, and Network information. """ - if config.get("GlobalPoolDetails") is not None: + if config.get("global_pool_details") is not None: self.get_have_global_pool(config).check_return_status() - if config.get("ReservePoolDetails") is not None: + if config.get("reserve_pool_details") is not None: self.get_have_reserve_pool(config).check_return_status() - if config.get("NetworkManagementDetails") is not None: + if config.get("network_management_details") is not None: self.get_have_network(config).check_return_status() self.log("Global Pool, Reserve Pool, Network Details in Cisco DNA Center " + str(self.have)) @@ -1129,23 +1157,22 @@ def get_want_global_pool(self, global_ippool): Check the return value of the API with check_return_status() Parameters: - - global_ippool (dict): Playbook global pool details containing - IpAddressSpace, DHCP server IPs, DNS server IPs, - IP pool name, IP pool CIDR, gateway, and type. + global_ippool (dict) - Playbook global pool details containing IpAddressSpace, + DHCP server IPs, DNS server IPs, IP pool name, IP pool CIDR, gateway, and type. Returns: - - self: Current object with updated desired Global Pool information. + self - The current object with updated desired Global Pool information. """ # Initialize the desired Global Pool configuration want_global = { "settings": { "ippool": [{ - "IpAddressSpace": global_ippool.get("IpAddressSpace"), - "dhcpServerIps": global_ippool.get("dhcpServerIps"), - "dnsServerIps": global_ippool.get("dnsServerIps"), - "ipPoolName": global_ippool.get("ipPoolName"), - "ipPoolCidr": global_ippool.get("ipPoolCidr"), + "IpAddressSpace": global_ippool.get("ip_address_space"), + "dhcpServerIps": global_ippool.get("dhcp_server_ips"), + "dnsServerIps": global_ippool.get("dns_server_ips"), + "ipPoolName": global_ippool.get("ip_pool_name"), + "ipPoolCidr": global_ippool.get("ip_pool_cidr"), "gateway": global_ippool.get("gateway"), "type": global_ippool.get("type"), }] @@ -1195,31 +1222,30 @@ def get_want_reserve_pool(self, reserve_pool): Check the return value of the API with check_return_status() Parameters: - - reserve_pool (dict) - Playbook reserved pool details - containing various properties. + reserve_pool (dict) - Playbook reserved pool + details containing various properties. Returns: - - self: The current object with updated desired Reserved - Pool information. + self - The current object with updated desired Reserved Pool information. """ want_reserve = { "name": reserve_pool.get("name"), "type": reserve_pool.get("type"), - "ipv6AddressSpace": reserve_pool.get("ipv6AddressSpace"), - "ipv4GlobalPool": reserve_pool.get("ipv4GlobalPool"), - "ipv4Prefix": reserve_pool.get("ipv4Prefix"), - "ipv4PrefixLength": reserve_pool.get("ipv4PrefixLength"), + "ipv6AddressSpace": reserve_pool.get("ipv6_address_space"), + "ipv4GlobalPool": reserve_pool.get("ipv4_global_pool"), + "ipv4Prefix": reserve_pool.get("ipv4_prefix"), + "ipv4PrefixLength": reserve_pool.get("ipv4_prefix_length"), "ipv4GateWay": reserve_pool.get("ipv4GateWay"), "ipv4DhcpServers": reserve_pool.get("ipv4DhcpServers"), - "ipv4DnsServers": reserve_pool.get("ipv4DnsServers"), - "ipv4Subnet": reserve_pool.get("ipv4Subnet"), - "ipv6GlobalPool": reserve_pool.get("ipv6GlobalPool"), - "ipv6Prefix": reserve_pool.get("ipv6Prefix"), - "ipv6PrefixLength": reserve_pool.get("ipv6PrefixLength"), + "ipv4DnsServers": reserve_pool.get("ipv4_dns_servers"), + "ipv4Subnet": reserve_pool.get("ipv4_subnet"), + "ipv6GlobalPool": reserve_pool.get("ipv6_global_pool"), + "ipv6Prefix": reserve_pool.get("ipv6_prefix"), + "ipv6PrefixLength": reserve_pool.get("ipv6_prefix_length"), "ipv6GateWay": reserve_pool.get("ipv6GateWay"), "ipv6DhcpServers": reserve_pool.get("ipv6DhcpServers"), - "ipv6Subnet": reserve_pool.get("ipv6Subnet"), + "ipv6Subnet": reserve_pool.get("ipv6_subnet"), "ipv6DnsServers": reserve_pool.get("ipv6DnsServers"), "ipv4TotalHost": reserve_pool.get("ipv4TotalHost"), "ipv6TotalHost": reserve_pool.get("ipv6TotalHost") @@ -1227,23 +1253,23 @@ def get_want_reserve_pool(self, reserve_pool): # Check for missing mandatory parameters in the playbook if not want_reserve.get("name"): - self.msg = "Missing mandatory parameter 'name' in ReservePoolDetails" + self.msg = "Missing mandatory parameter 'name' in reserve_pool_details" self.status = "failed" return self if want_reserve.get("ipv4Prefix") is True: if want_reserve.get("ipv4Subnet") is None and \ want_reserve.get("ipv4TotalHost") is None: - self.msg = "missing parameter 'ipv4Subnet' or 'ipv4TotalHost' \ - while adding the ipv4 in ReservePoolDetails" + self.msg = "missing parameter 'ipv4_subnet' or 'ipv4TotalHost' \ + while adding the ipv4 in reserve_pool_details" self.status = "failed" return self if want_reserve.get("ipv6Prefix") is True: if want_reserve.get("ipv6Subnet") is None and \ want_reserve.get("ipv6TotalHost") is None: - self.msg = "missing parameter 'ipv6Subnet' or 'ipv6TotalHost' \ - while adding the ipv6 in ReservePoolDetails" + self.msg = "missing parameter 'ipv6_subnet' or 'ipv6TotalHost' \ + while adding the ipv6 in reserve_pool_details" self.status = "failed" return self @@ -1252,12 +1278,12 @@ def get_want_reserve_pool(self, reserve_pool): # If there are no existing Reserved Pool details, validate and set defaults if not self.have.get("reservePool").get("details"): if not want_reserve.get("ipv4GlobalPool"): - self.msg = "missing parameter 'ipv4GlobalPool' in ReservePoolDetails" + self.msg = "missing parameter 'ipv4GlobalPool' in reserve_pool_details" self.status = "failed" return self if not want_reserve.get("ipv4PrefixLength"): - self.msg = "missing parameter 'ipv4PrefixLength' in ReservePoolDetails" + self.msg = "missing parameter 'ipv4_prefix_length' in reserve_pool_details" self.status = "failed" return self @@ -1308,11 +1334,11 @@ def get_want_network(self, network_management_details): Check the return value of the API with check_return_status() Parameters: - - network_management_details (dict): Playbook network - details containing various network settings. + network_management_details (dict) - Playbook network + details containing various network settings. Returns: - - self: Current object with updated desired Network-related info. + self - The current object with updated desired Network-related information. """ want_network = { @@ -1330,16 +1356,17 @@ def get_want_network(self, network_management_details): } } want_network_settings = want_network.get("settings") - if network_management_details.get("dhcpServer"): + self.log(str(self.have)) + if network_management_details.get("dhcp_server") is not None: want_network_settings.update({ - "dhcpServer": network_management_details.get("dhcpServer") + "dhcpServer": network_management_details.get("dhcp_server") }) else: del want_network_settings["dhcpServer"] - if network_management_details.get("ntpServer"): + if network_management_details.get("ntp_server") is not None: want_network_settings.update({ - "ntpServer": network_management_details.get("ntpServer") + "ntpServer": network_management_details.get("ntp_server") }) else: del want_network_settings["ntpServer"] @@ -1352,99 +1379,94 @@ def get_want_network(self, network_management_details): self.status = "failed" return self - dnsServer = network_management_details.get("dnsServer") - if dnsServer: - if dnsServer.get("domainName"): + dnsServer = network_management_details.get("dns_server") + if dnsServer is not None: + if dnsServer.get("domain_name") is not None: want_network_settings.get("dnsServer").update({ "domainName": - dnsServer.get("domainName") + dnsServer.get("domain_name") }) - if dnsServer.get("primaryIpAddress"): + if dnsServer.get("primary_ip_address") is not None: want_network_settings.get("dnsServer").update({ "primaryIpAddress": - dnsServer.get("primaryIpAddress") + dnsServer.get("primary_ip_address") }) - if dnsServer.get("secondaryIpAddress"): + if dnsServer.get("secondary_ip_address") is not None: want_network_settings.get("dnsServer").update({ "secondaryIpAddress": - dnsServer.get("secondaryIpAddress") + dnsServer.get("secondary_ip_address") }) else: del want_network_settings["dnsServer"] - snmpServer = network_management_details.get("snmpServer") - if snmpServer: - if snmpServer.get("configureDnacIP"): + snmpServer = network_management_details.get("snmp_server") + if snmpServer is not None: + if snmpServer.get("configure_dnac_ip") is not None: want_network_settings.get("snmpServer").update({ - "configureDnacIP": snmpServer.get("configureDnacIP") + "configureDnacIP": snmpServer.get("configure_dnac_ip") }) - if snmpServer.get("ipAddresses"): + if snmpServer.get("ip_addresses") is not None: want_network_settings.get("snmpServer").update({ - "ipAddresses": snmpServer.get("ipAddresses") + "ipAddresses": snmpServer.get("ip_addresses") }) else: del want_network_settings["snmpServer"] syslogServer = network_management_details.get("syslogServer") - if syslogServer: - if syslogServer.get("configureDnacIP"): + if syslogServer is not None: + if syslogServer.get("configure_dnac_ip") is not None: want_network_settings.get("syslogServer").update({ - "configureDnacIP": syslogServer.get("configureDnacIP") + "configureDnacIP": syslogServer.get("configure_dnac_ip") }) - if syslogServer.get("ipAddresses"): + if syslogServer.get("ip_addresses") is not None: want_network_settings.get("syslogServer").update({ - "ipAddresses": syslogServer.get("ipAddresses") + "ipAddresses": syslogServer.get("ip_addresses") }) else: del want_network_settings["syslogServer"] - netflowcollector = network_management_details.get("netflowcollector") - if netflowcollector: - if netflowcollector.get("ipAddress"): + netflowcollector = network_management_details.get("netflow_collector") + if netflowcollector is not None: + if netflowcollector.get("ip_address") is not None: want_network_settings.get("netflowcollector").update({ "ipAddress": - netflowcollector.get("ipAddress") + netflowcollector.get("ip_address") }) - if netflowcollector.get("port"): + if netflowcollector.get("port") is not None: want_network_settings.get("netflowcollector").update({ "port": netflowcollector.get("port") }) - if netflowcollector.get("configureDnacIP"): - want_network_settings.get("netflowcollector").update({ - "configureDnacIP": - netflowcollector.get("configureDnacIP") - }) else: del want_network_settings["netflowcollector"] - messageOfTheday = network_management_details.get("messageOfTheday") - if messageOfTheday: - if messageOfTheday.get("bannerMessage"): + messageOfTheday = network_management_details.get("message_of_the_day") + if messageOfTheday is not None: + if messageOfTheday.get("banner_message") is not None: want_network_settings.get("messageOfTheday").update({ "bannerMessage": - messageOfTheday.get("bannerMessage") + messageOfTheday.get("banner_message") }) - if messageOfTheday.get("retainExistingBanner"): + if messageOfTheday.get("retain_existing_banner") is not None: want_network_settings.get("messageOfTheday").update({ "retainExistingBanner": - messageOfTheday.get("retainExistingBanner") + messageOfTheday.get("retain_existing_banner") }) else: del want_network_settings["messageOfTheday"] network_aaa = network_management_details.get("network_aaa") if network_aaa: - if network_aaa.get("ipAddress"): + if network_aaa.get("ip_address"): want_network_settings.get("network_aaa").update({ "ipAddress": - network_aaa.get("ipAddress") + network_aaa.get("ip_address") }) else: if network_aaa.get("servers") == "ISE": - self.msg = "missing parameter ipAddress in network_aaa, server ISE is set" + self.msg = "missing parameter ip_address in network_aaa, server ISE is set" self.status = "failed" return self @@ -1477,24 +1499,24 @@ def get_want_network(self, network_management_details): self.status = "failed" return self - if network_aaa.get("sharedSecret"): + if network_aaa.get("shared_secret"): want_network_settings.get("network_aaa").update({ "sharedSecret": - network_aaa.get("sharedSecret") + network_aaa.get("shared_secret") }) else: del want_network_settings["network_aaa"] - clientAndEndpoint_aaa = network_management_details.get("clientAndEndpoint_aaa") + clientAndEndpoint_aaa = network_management_details.get("client_and_endpoint_aaa") if clientAndEndpoint_aaa: - if clientAndEndpoint_aaa.get("ipAddress"): + if clientAndEndpoint_aaa.get("ip_address"): want_network_settings.get("clientAndEndpoint_aaa").update({ "ipAddress": - clientAndEndpoint_aaa.get("ipAddress") + clientAndEndpoint_aaa.get("ip_address") }) else: if clientAndEndpoint_aaa.get("servers") == "ISE": - self.msg = "missing parameter ipAddress in clientAndEndpoint_aaa, \ + self.msg = "missing parameter ip_address in clientAndEndpoint_aaa, \ server ISE is set" self.status = "failed" return self @@ -1529,10 +1551,10 @@ def get_want_network(self, network_management_details): self.status = "failed" return self - if clientAndEndpoint_aaa.get("sharedSecret"): + if clientAndEndpoint_aaa.get("shared_secret"): want_network_settings.get("clientAndEndpoint_aaa").update({ "sharedSecret": - clientAndEndpoint_aaa.get("sharedSecret") + clientAndEndpoint_aaa.get("shared_secret") }) else: del want_network_settings["clientAndEndpoint_aaa"] @@ -1548,21 +1570,22 @@ def get_want(self, config): Get all the Global Pool Reserved Pool and Network related information from playbook Parameters: - - config (list of dict): Playbook details + config (list of dict) - Playbook details + Returns: - - self + None """ - if config.get("GlobalPoolDetails"): - global_ippool = config.get("GlobalPoolDetails").get("settings").get("ippool")[0] + if config.get("global_pool_details"): + global_ippool = config.get("global_pool_details").get("settings").get("ippool")[0] self.get_want_global_pool(global_ippool).check_return_status() - if config.get("ReservePoolDetails"): - reserve_pool = config.get("ReservePoolDetails") + if config.get("reserve_pool_details"): + reserve_pool = config.get("reserve_pool_details") self.get_want_reserve_pool(reserve_pool).check_return_status() - if config.get("NetworkManagementDetails"): - network_management_details = config.get("NetworkManagementDetails") \ + if config.get("network_management_details"): + network_management_details = config.get("network_management_details") \ .get("settings") self.get_want_network(network_management_details).check_return_status() @@ -1576,14 +1599,14 @@ def update_global_pool(self, config): Update/Create Global Pool in Cisco DNA Center with fields provided in playbook Parameters: - - config (list of dict): Playbook details + config (list of dict) - Playbook details Returns: - - None + None """ - name = config.get("GlobalPoolDetails") \ - .get("settings").get("ippool")[0].get("ipPoolName") + name = config.get("global_pool_details") \ + .get("settings").get("ippool")[0].get("ip_pool_name") result_global_pool = self.result.get("response")[0].get("globalPool") result_global_pool.get("response").update({name: {}}) @@ -1660,13 +1683,13 @@ def update_reserve_pool(self, config): If it exists and requires an update, it updates the pool. If not, it creates a new pool. Parameters: - - config (list of dict) - Playbook details containing Reserve - Pool information. + config (list of dict) - Playbook details containing Reserve Pool information. + Returns: - - None + None """ - name = config.get("ReservePoolDetails").get("name") + name = config.get("reserve_pool_details").get("name") result_reserve_pool = self.result.get("response")[1].get("reservePool") result_reserve_pool.get("response").update({name: {}}) self.log("Reserve Pool Cisco DNA Center Details " + @@ -1676,7 +1699,7 @@ def update_reserve_pool(self, config): # Check pool exist, if not create and return self.log(str(self.want.get("wantReserve").get("ipv4GlobalPool"))) - site_name = config.get("ReservePoolDetails").get("siteName") + site_name = config.get("reserve_pool_details").get("site_name") reserve_params = self.want.get("wantReserve") site_id = self.get_site_id(site_name) reserve_params.update({"site_id": site_id}) @@ -1746,18 +1769,18 @@ def update_network(self, config): Center based on the provided playbook details. Parameters: - - config (list of dict) - Playbook details containing Network - Management information. + config (list of dict) - Playbook details containing Network Management information. + Returns: - - None + None """ - siteName = config.get("NetworkManagementDetails").get("siteName") + site_name = config.get("network_management_details").get("site_name") result_network = self.result.get("response")[2].get("network") - result_network.get("response").update({siteName: {}}) + result_network.get("response").update({site_name: {}}) obj_params = [ ("settings", "settings"), - ("siteName", "siteName") + ("site_name", "site_name") ] # Check update is required or not @@ -1765,11 +1788,11 @@ def update_network(self, config): self.want.get("wantNetwork"), obj_params): self.log("Network doesn't require an update") - result_network.get("response").get(siteName).update({ + result_network.get("response").get(site_name).update({ "Cisco DNA Center params": self.have.get("network") .get("net_details").get("settings") }) - result_network.get("msg").update({siteName: "Network doesn't require an update"}) + result_network.get("msg").update({site_name: "Network doesn't require an update"}) return self.log("Network requires update") @@ -1780,15 +1803,16 @@ def update_network(self, config): net_params.update({"site_id": self.have.get("network").get("site_id")}) response = self.dnac._exec( family="network_settings", - function='update_network', + function='update_network_v2', params=net_params, ) self.log(str(response)) - self.check_execution_response_status(response).check_return_status() + validation_string = "desired common settings operation successful" + self.check_task_response_status(response, validation_string).check_return_status() self.log("Network has been changed Successfully") result_network.get("msg") \ - .update({siteName: "Network Updated successfully"}) - result_network.get("response").get(siteName) \ + .update({site_name: "Network Updated successfully"}) + result_network.get("response").get(site_name) \ .update({"Network Details": self.want.get("wantNetwork").get("settings")}) return @@ -1798,20 +1822,20 @@ def get_diff_merged(self, config): Network configurations in Cisco DNA Center based on the playbook details Parameters: - - config (list of dict) - Playbook details containing - Global Pool, Reserve Pool, and Network Management information. + config (list of dict) - Playbook details containing + Global Pool, Reserve Pool, and Network Management information. Returns: - - self + self """ - if config.get("GlobalPoolDetails") is not None: + if config.get("global_pool_details") is not None: self.update_global_pool(config) - if config.get("ReservePoolDetails") is not None: + if config.get("reserve_pool_details") is not None: self.update_reserve_pool(config) - if config.get("NetworkManagementDetails") is not None: + if config.get("network_management_details") is not None: self.update_network(config) return self @@ -1821,21 +1845,23 @@ def delete_reserve_pool(self, name): Delete a Reserve Pool by name in Cisco DNA Center Parameters: - - name (str): The name of the Reserve Pool to be deleted. + name (str) - The name of the Reserve Pool to be deleted. Returns: - - self + self """ reserve_pool_exists = self.have.get("reservePool").get("exists") - self.log("Reserved Ip Pool to be deleted " + - str(self.want.get("wantReserve").get("name"))) + result_reserve_pool = self.result.get("response")[1].get("reservePool") if not reserve_pool_exists: + result_reserve_pool.get("response").update({name: "Reserve Pool not found"}) self.msg = "Reserved Ip Subpool Not Found" - self.status = "failed" + self.status = "success" return self + self.log("Reserved Ip Pool to be deleted " + + str(self.have.get("reservePool").get("name"))) _id = self.have.get("reservePool").get("id") self.log("Reserve pool {0} id ".format(name) + str(_id)) response = self.dnac._exec( @@ -1860,16 +1886,18 @@ def delete_global_pool(self, name): Delete a Global Pool by name in Cisco DNA Center Parameters: - - name (str): The name of the Global Pool to be deleted. + name (str) - The name of the Global Pool to be deleted. Returns: - - self + self """ global_pool_exists = self.have.get("globalPool").get("exists") + result_global_pool = self.result.get("response")[0].get("globalPool") if not global_pool_exists: + result_global_pool.get("response").update({name: "Global Pool not found"}) self.msg = "Global pool Not Found" - self.status = "failed" + self.status = "success" return self response = self.dnac._exec( @@ -1893,23 +1921,22 @@ def delete_global_pool(self, name): def get_diff_deleted(self, config): """ - Delete Reserve Pool and Global Pool in Cisco DNA Center based - on playbook details. + Delete Reserve Pool and Global Pool in Cisco DNA Center based on playbook details. Parameters: - - config (list of dict): Playbook details + config (list of dict) - Playbook details Returns: - - self + self """ - if config.get("ReservePoolDetails") is not None: - name = config.get("ReservePoolDetails").get("name") + if config.get("reserve_pool_details") is not None: + name = config.get("reserve_pool_details").get("name") self.delete_reserve_pool(name).check_return_status() - if config.get("GlobalPoolDetails") is not None: - name = config.get("GlobalPoolDetails") \ - .get("settings").get("ippool")[0].get("ipPoolName") + if config.get("global_pool_details") is not None: + name = config.get("global_pool_details") \ + .get("settings").get("ippool")[0].get("ip_pool_name") self.delete_global_pool(name).check_return_status() return self @@ -1919,10 +1946,10 @@ def reset_values(self): Reset all neccessary attributes to default values Parameters: - - self + None Returns: - - None + None """ self.have.clear() @@ -1962,7 +1989,8 @@ def main(): for config in dnac_network.config: dnac_network.reset_values() dnac_network.get_have(config).check_return_status() - dnac_network.get_want(config).check_return_status() + if state != "deleted": + dnac_network.get_want(config).check_return_status() dnac_network.get_diff_state_apply[state](config).check_return_status() module.exit_json(**dnac_network.result) diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index cf6a0a50c5..79ed72052f 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -17,11 +17,17 @@ - API to create a template by project name and template name. - API to update a template by template name and project name. - API to delete a template by template name and project name. +- API to export the projects for given projectNames. +- API to export the templates for given templateIds. +- API to manage operation create of the resource Configuration Template Import Project. +- API to manage operation create of the resource Configuration Template Import Template. version_added: '6.6.0' extends_documentation_fragment: - cisco.dnac.intent_params author: Madhan Sankaranarayanan (@madhansansel) Rishita Chowdhary (@rishitachowdhary) + Akash Bhaskaran (@akabhask) + Muthu Rakesh (@MUTHU-RAKESH-27) options: state: description: The state of DNAC after module completion. @@ -35,25 +41,255 @@ elements: dict required: true suboptions: - author: - description: Author of template. - type: str - composite: - description: Is it composite template. - type: bool - containingTemplates: - description: Configuration Template Create's containingTemplates. + configuration_templates: + description: Create/Update/Delete template. + type: dict suboptions: + author: + description: Author of template. + type: str composite: description: Is it composite template. type: bool - description: - description: Description of template. - type: str - deviceTypes: - description: deviceTypes on which templates would be applied. + containing_templates: + description: Configuration Template Create's containingTemplates. + suboptions: + composite: + description: Is it composite template. + type: bool + description: + description: Description of template. + type: str + device_types: + description: deviceTypes on which templates would be applied. + type: list + elements: dict + suboptions: + productFamily: + description: Device family. + type: str + productSeries: + description: Device series. + type: str + productType: + description: Device type. + type: str + id: + description: UUID of template. + type: str + language: + description: Template language + choices: + - JINJA + - VELOCITY + type: str + name: + description: Name of template. + type: str + project_name: + description: Name of the project under which templates are managed. + type: str + projectDescription: + description: Description of the project created. + type: str + rollbackTemplateParams: + description: Params required for template rollback. + type: list + elements: dict + suboptions: + binding: + description: Bind to source. + type: str + customOrder: + description: CustomOrder of template param. + type: int + dataType: + description: Datatype of template param. + type: str + defaultValue: + description: Default value of template param. + type: str + description: + description: Description of template param. + type: str + displayName: + description: Display name of param. + type: str + group: + description: Group. + type: str + id: + description: UUID of template param. + type: str + instructionText: + description: Instruction text for param. + type: str + key: + description: Key. + type: str + notParam: + description: Is it not a variable. + type: bool + order: + description: Order of template param. + type: int + paramArray: + description: Is it an array. + type: bool + parameterName: + description: Name of template param. + type: str + provider: + description: Provider. + type: str + range: + description: Configuration Template Create's range. + type: list + elements: dict + suboptions: + id: + description: UUID of range. + type: str + maxValue: + description: Max value of range. + type: int + minValue: + description: Min value of range. + type: int + required: + description: Is param required. + type: bool + selection: + description: Configuration Template Create's selection. + suboptions: + defaultSelectedValues: + description: Default selection values. + elements: str + type: list + id: + description: UUID of selection. + type: str + selectionType: + description: Type of selection(SINGLE_SELECT or MULTI_SELECT). + type: str + selectionValues: + description: Selection values. + type: dict + type: dict + tags: + description: Configuration Template Create's tags. + suboptions: + id: + description: UUID of tag. + type: str + name: + description: Name of tag. + type: str + type: list + elements: dict + template_content: + description: Template content. + type: str + templateParams: + description: Configuration Template Create's templateParams. + elements: dict + suboptions: + binding: + description: Bind to source. + type: str + customOrder: + description: CustomOrder of template param. + type: int + dataType: + description: Datatype of template param. + type: str + defaultValue: + description: Default value of template param. + type: str + description: + description: Description of template param. + type: str + displayName: + description: Display name of param. + type: str + group: + description: Group. + type: str + id: + description: UUID of template param. + type: str + instructionText: + description: Instruction text for param. + type: str + key: + description: Key. + type: str + notParam: + description: Is it not a variable. + type: bool + order: + description: Order of template param. + type: int + paramArray: + description: Is it an array. + type: bool + parameterName: + description: Name of template param. + type: str + provider: + description: Provider. + type: str + range: + description: Configuration Template Create's range. + suboptions: + id: + description: UUID of range. + type: str + maxValue: + description: Max value of range. + type: int + minValue: + description: Min value of range. + type: int + type: list + elements: dict + required: + description: Is param required. + type: bool + selection: + description: Configuration Template Create's selection. + suboptions: + defaultSelectedValues: + description: Default selection values. + elements: str + type: list + id: + description: UUID of selection. + type: str + selectionType: + description: Type of selection(SINGLE_SELECT or MULTI_SELECT). + type: str + selectionValues: + description: Selection values. + type: dict + type: dict + type: list + version: + description: Current version of template. + type: str type: list elements: dict + create_time: + description: Create time of template. + type: int + custom_params_order: + description: Custom Params Order. + type: bool + template_description: + description: Description of template. + type: str + device_types: + description: Configuration Template Create's deviceTypes. This field is mandatory to create a new template. suboptions: productFamily: description: Device family. @@ -64,8 +300,10 @@ productType: description: Device type. type: str - id: - description: UUID of template. + type: list + elements: dict + failure_policy: + description: Define failure policy if template provisioning fails. type: str language: description: Template language @@ -73,20 +311,32 @@ - JINJA - VELOCITY type: str - name: - description: Name of template. + last_update_time: + description: Update time of template. + type: int + latest_version_time: + description: Latest versioned template time. + type: int + template_name: + description: Name of template. This field is mandatory to create a new template. + type: str + parent_template_id: + description: Parent templateID. type: str - projectName: - description: Name of the project under which templates are managed. + project_id: + description: Project UUID. + type: str + project_name: + description: Project name. type: str - required: true projectDescription: - description: Description of the project created. + description: Project Description. type: str - rollbackTemplateParams: - description: Params required for template rollback. - type: list - elements: dict + rollback_template_content: + description: Rollback template content. + type: str + rollback_template_params: + description: Configuration Template Create's rollbackTemplateParams. suboptions: binding: description: Bind to source. @@ -135,8 +385,6 @@ type: str range: description: Configuration Template Create's range. - type: list - elements: dict suboptions: id: description: UUID of range. @@ -147,6 +395,8 @@ minValue: description: Min value of range. type: int + type: list + elements: dict required: description: Is param required. type: bool @@ -167,7 +417,18 @@ description: Selection values. type: dict type: dict - tags: + type: list + elements: dict + software_type: + description: Applicable device software type. This field is mandatory to create a new template. + type: str + software_variant: + description: Applicable device software variant. + type: str + software_version: + description: Applicable device software version. + type: str + template_tag: description: Configuration Template Create's tags. suboptions: id: @@ -178,12 +439,11 @@ type: str type: list elements: dict - templateContent: + template_content: description: Template content. type: str - templateParams: + template_params: description: Configuration Template Create's templateParams. - elements: dict suboptions: binding: description: Bind to source. @@ -265,282 +525,575 @@ type: dict type: dict type: list - version: - description: Current version of template. - type: str - type: list - elements: dict - createTime: - description: Create time of template. - type: int - customParamsOrder: - description: Custom Params Order. - type: bool - template_description: - description: Description of template. - type: str - deviceTypes: - description: Configuration Template Create's deviceTypes. This field is mandatory to create a new template. - suboptions: - productFamily: - description: Device family. - type: str - productSeries: - description: Device series. - type: str - productType: - description: Device type. - type: str - type: list - elements: dict - failurePolicy: - description: Define failure policy if template provisioning fails. - type: str - language: - description: Template language - choices: - - JINJA - - VELOCITY - type: str - lastUpdateTime: - description: Update time of template. - type: int - latestVersionTime: - description: Latest versioned template time. - type: int - templateName: - description: Name of template. This field is mandatory to create a new template. - type: str - parentTemplateId: - description: Parent templateID. - type: str - projectId: - description: Project UUID. - type: str - projectName: - description: Project name. - type: str - projectDescription: - description: Project Description. - type: str - rollbackTemplateContent: - description: Rollback template content. - type: str - rollbackTemplateParams: - description: Configuration Template Create's rollbackTemplateParams. - suboptions: - binding: - description: Bind to source. - type: str - customOrder: - description: CustomOrder of template param. - type: int - dataType: - description: Datatype of template param. - type: str - defaultValue: - description: Default value of template param. - type: str - description: - description: Description of template param. - type: str - displayName: - description: Display name of param. - type: str - group: - description: Group. - type: str - id: - description: UUID of template param. - type: str - instructionText: - description: Instruction text for param. - type: str - key: - description: Key. - type: str - notParam: - description: Is it not a variable. - type: bool - order: - description: Order of template param. - type: int - paramArray: - description: Is it an array. - type: bool - parameterName: - description: Name of template param. - type: str - provider: - description: Provider. - type: str - range: - description: Configuration Template Create's range. - suboptions: - id: - description: UUID of range. - type: str - maxValue: - description: Max value of range. - type: int - minValue: - description: Min value of range. - type: int - type: list elements: dict - required: - description: Is param required. - type: bool - selection: - description: Configuration Template Create's selection. + validation_errors: + description: Configuration Template Create's validationErrors. suboptions: - defaultSelectedValues: - description: Default selection values. - elements: str + rollbackTemplateErrors: + description: Validation or design conflicts errors of rollback template. + elements: dict type: list - id: - description: UUID of selection. + templateErrors: + description: Validation or design conflicts errors. + elements: dict + type: list + templateId: + description: UUID of template. type: str - selectionType: - description: Type of selection(SINGLE_SELECT or MULTI_SELECT). + templateVersion: + description: Current version of template. type: str - selectionValues: - description: Selection values. - type: dict type: dict - type: list - elements: dict - softwareType: - description: Applicable device software type. This field is mandatory to create a new template. - type: str - softwareVariant: - description: Applicable device software variant. - type: str - softwareVersion: - description: Applicable device software version. - type: str - template_tag: - description: Configuration Template Create's tags. - suboptions: - id: - description: UUID of tag. + version: + description: Current version of template. type: str - name: - description: Name of tag. + version_description: + description: Template version comments. type: str - type: list - elements: dict - templateContent: - description: Template content. - type: str - templateParams: - description: Configuration Template Create's templateParams. + export: + description: Export the project/template details. + type: dict suboptions: - binding: - description: Bind to source. - type: str - customOrder: - description: CustomOrder of template param. - type: int - dataType: - description: Datatype of template param. - type: str - defaultValue: - description: Default value of template param. - type: str - description: - description: Description of template param. - type: str - displayName: - description: Display name of param. - type: str - group: - description: Group. - type: str - id: - description: UUID of template param. - type: str - instructionText: - description: Instruction text for param. - type: str - key: - description: Key. - type: str - notParam: - description: Is it not a variable. - type: bool - order: - description: Order of template param. - type: int - paramArray: - description: Is it an array. - type: bool - parameterName: - description: Name of template param. - type: str - provider: - description: Provider. - type: str - range: - description: Configuration Template Create's range. - suboptions: - id: - description: UUID of range. - type: str - maxValue: - description: Max value of range. - type: int - minValue: - description: Min value of range. - type: int + project: + description: Export the project. + type: list + elements: str + template: + description: Export the template. type: list elements: dict - required: - description: Is param required. - type: bool - selection: - description: Configuration Template Create's selection. suboptions: - defaultSelectedValues: - description: Default selection values. - elements: str - type: list - id: - description: UUID of selection. + project_name: + description: Name of the project under the template available. type: str - selectionType: - description: Type of selection(SINGLE_SELECT or MULTI_SELECT). + template_name: + description: Name of the template which we need to export type: str - selectionValues: - description: Selection values. - type: dict - type: dict - type: list - elements: dict - validationErrors: - description: Configuration Template Create's validationErrors. + import: + description: Import the project/template details. + type: suboptions: - rollbackTemplateErrors: - description: Validation or design conflicts errors of rollback template. - elements: dict - type: list - templateErrors: - description: Validation or design conflicts errors. - elements: dict - type: list - templateId: - description: UUID of template. - type: str - templateVersion: - description: Current version of template. - type: str - type: dict - version: - description: Current version of template. - type: str - versionDescription: - description: Template version comments. - type: str + project: + description: Import the project details. + type: dict + suboptions: + doVersion: + description: DoVersion query parameter. If this flag is true, creates a new + version of the template with the imported contents, if the templates already + exists. " If false and if template already exists, then operation + fails with 'Template already exists' error. + type: bool + template: + description: Import the template details. + type: dict + suboptions: + doVersion: + description: DoVersion query parameter. If this flag is true, creates a new + version of the template with the imported contents, if the templates already + exists. " If false and if template already exists, then operation + fails with 'Template already exists' error. + type: bool + payload: + description: Configuration Template Import Template's payload. + elements: dict + suboptions: + author: + description: Author of template. + type: str + composite: + description: Is it composite template. + type: bool + containingTemplates: + description: Configuration Template Import Template's containingTemplates. + elements: dict + suboptions: + composite: + description: Is it composite template. + type: bool + description: + description: Description of template. + type: str + device_types: + description: Configuration Template Import Template's deviceTypes. + elements: dict + suboptions: + productFamily: + description: Device family. + type: str + productSeries: + description: Device series. + type: str + productType: + description: Device type. + type: str + type: list + id: + description: UUID of template. + type: str + language: + description: Template language (JINJA or VELOCITY). + type: str + name: + description: Name of template. + type: str + project_name: + description: Project name. + type: str + rollbackTemplateParams: + description: Configuration Template Import Template's rollbackTemplateParams. + elements: dict + suboptions: + binding: + description: Bind to source. + type: str + customOrder: + description: CustomOrder of template param. + type: int + dataType: + description: Datatype of template param. + type: str + defaultValue: + description: Default value of template param. + type: str + description: + description: Description of template param. + type: str + displayName: + description: Display name of param. + type: str + group: + description: Group. + type: str + id: + description: UUID of template param. + type: str + instructionText: + description: Instruction text for param. + type: str + key: + description: Key. + type: str + notParam: + description: Is it not a variable. + type: bool + order: + description: Order of template param. + type: int + paramArray: + description: Is it an array. + type: bool + parameterName: + description: Name of template param. + type: str + provider: + description: Provider. + type: str + range: + description: Configuration Template Import Template's range. + elements: dict + suboptions: + id: + description: UUID of range. + type: str + maxValue: + description: Max value of range. + type: int + minValue: + description: Min value of range. + type: int + type: list + required: + description: Is param required. + type: bool + selection: + description: Configuration Template Import Template's selection. + suboptions: + defaultSelectedValues: + description: Default selection values. + elements: str + type: list + id: + description: UUID of selection. + type: str + selectionType: + description: Type of selection(SINGLE_SELECT or MULTI_SELECT). + type: str + selectionValues: + description: Selection values. + type: dict + type: dict + type: list + tags: + description: Configuration Template Import Template's tags. + elements: dict + suboptions: + id: + description: UUID of tag. + type: str + name: + description: Name of tag. + type: str + type: list + template_content: + description: Template content. + type: str + templateParams: + description: Configuration Template Import Template's templateParams. + elements: dict + suboptions: + binding: + description: Bind to source. + type: str + customOrder: + description: CustomOrder of template param. + type: int + dataType: + description: Datatype of template param. + type: str + defaultValue: + description: Default value of template param. + type: str + description: + description: Description of template param. + type: str + displayName: + description: Display name of param. + type: str + group: + description: Group. + type: str + id: + description: UUID of template param. + type: str + instructionText: + description: Instruction text for param. + type: str + key: + description: Key. + type: str + notParam: + description: Is it not a variable. + type: bool + order: + description: Order of template param. + type: int + paramArray: + description: Is it an array. + type: bool + parameterName: + description: Name of template param. + type: str + provider: + description: Provider. + type: str + range: + description: Configuration Template Import Template's range. + elements: dict + suboptions: + id: + description: UUID of range. + type: str + maxValue: + description: Max value of range. + type: int + minValue: + description: Min value of range. + type: int + type: list + required: + description: Is param required. + type: bool + selection: + description: Configuration Template Import Template's selection. + suboptions: + defaultSelectedValues: + description: Default selection values. + elements: str + type: list + id: + description: UUID of selection. + type: str + selectionType: + description: Type of selection(SINGLE_SELECT or MULTI_SELECT). + type: str + selectionValues: + description: Selection values. + type: dict + type: dict + type: list + version: + description: Current version of template. + type: str + type: list + createTime: + description: Create time of template. + type: int + customParamsOrder: + description: Custom Params Order. + type: bool + description: + description: Description of template. + type: str + device_types: + description: Configuration Template Import Template's deviceTypes. + elements: dict + suboptions: + productFamily: + description: Device family. + type: str + productSeries: + description: Device series. + type: str + productType: + description: Device type. + type: str + type: list + failurePolicy: + description: Define failure policy if template provisioning fails. + type: str + id: + description: UUID of template. + type: str + language: + description: Template language (JINJA or VELOCITY). + type: str + lastUpdateTime: + description: Update time of template. + type: int + latestVersionTime: + description: Latest versioned template time. + type: int + name: + description: Name of template. + type: str + parentTemplateId: + description: Parent templateID. + type: str + projectId: + description: Project UUID. + type: str + project_name: + description: Project name. + type: str + rollbackTemplateContent: + description: Rollback template content. + type: str + rollbackTemplateParams: + description: Configuration Template Import Template's rollbackTemplateParams. + elements: dict + suboptions: + binding: + description: Bind to source. + type: str + customOrder: + description: CustomOrder of template param. + type: int + dataType: + description: Datatype of template param. + type: str + defaultValue: + description: Default value of template param. + type: str + description: + description: Description of template param. + type: str + displayName: + description: Display name of param. + type: str + group: + description: Group. + type: str + id: + description: UUID of template param. + type: str + instructionText: + description: Instruction text for param. + type: str + key: + description: Key. + type: str + notParam: + description: Is it not a variable. + type: bool + order: + description: Order of template param. + type: int + paramArray: + description: Is it an array. + type: bool + parameterName: + description: Name of template param. + type: str + provider: + description: Provider. + type: str + range: + description: Configuration Template Import Template's range. + elements: dict + suboptions: + id: + description: UUID of range. + type: str + maxValue: + description: Max value of range. + type: int + minValue: + description: Min value of range. + type: int + type: list + required: + description: Is param required. + type: bool + selection: + description: Configuration Template Import Template's selection. + suboptions: + defaultSelectedValues: + description: Default selection values. + elements: str + type: list + id: + description: UUID of selection. + type: str + selectionType: + description: Type of selection(SINGLE_SELECT or MULTI_SELECT). + type: str + selectionValues: + description: Selection values. + type: dict + type: dict + type: list + software_type: + description: Applicable device software type. + type: str + software_variant: + description: Applicable device software variant. + type: str + softwareVersion: + description: Applicable device software version. + type: str + tags: + description: Configuration Template Import Template's tags. + elements: dict + suboptions: + id: + description: UUID of tag. + type: str + name: + description: Name of tag. + type: str + type: list + template_content: + description: Template content. + type: str + templateParams: + description: Configuration Template Import Template's templateParams. + elements: dict + suboptions: + binding: + description: Bind to source. + type: str + customOrder: + description: CustomOrder of template param. + type: int + dataType: + description: Datatype of template param. + type: str + defaultValue: + description: Default value of template param. + type: str + description: + description: Description of template param. + type: str + displayName: + description: Display name of param. + type: str + group: + description: Group. + type: str + id: + description: UUID of template param. + type: str + instructionText: + description: Instruction text for param. + type: str + key: + description: Key. + type: str + notParam: + description: Is it not a variable. + type: bool + order: + description: Order of template param. + type: int + paramArray: + description: Is it an array. + type: bool + parameterName: + description: Name of template param. + type: str + provider: + description: Provider. + type: str + range: + description: Configuration Template Import Template's range. + elements: dict + suboptions: + id: + description: UUID of range. + type: str + maxValue: + description: Max value of range. + type: int + minValue: + description: Min value of range. + type: int + type: list + required: + description: Is param required. + type: bool + selection: + description: Configuration Template Import Template's selection. + suboptions: + defaultSelectedValues: + description: Default selection values. + elements: str + type: list + id: + description: UUID of selection. + type: str + selectionType: + description: Type of selection(SINGLE_SELECT or MULTI_SELECT). + type: str + selectionValues: + description: Selection values. + type: dict + type: dict + type: list + validationErrors: + description: Configuration Template Import Template's validationErrors. + suboptions: + rollbackTemplateErrors: + description: Validation or design conflicts errors of rollback template. + type: dict + templateErrors: + description: Validation or design conflicts errors. + type: dict + templateId: + description: UUID of template. + type: str + templateVersion: + description: Current version of template. + type: str + type: dict + version: + description: Current version of template. + type: str + type: list + project_name: + description: ProjectName path parameter. Project name to create template under the + project. + type: str + requirements: - dnacentersdk == 2.4.5 - python >= 3.5 @@ -549,16 +1102,24 @@ configuration_templates.ConfigurationTemplates.create_template, configuration_templates.ConfigurationTemplates.deletes_the_template, configuration_templates.ConfigurationTemplates.update_template, + configuration_templates.ConfigurationTemplates.export_projects, + configuration_templates.ConfigurationTemplates.export_templates, + configuration_templates.ConfigurationTemplates.imports_the_projects_provided, + configuration_templates.ConfigurationTemplates.imports_the_templates_provided, - Paths used are post /dna/intent/api/v1/template-programmer/project/{projectId}/template, delete /dna/intent/api/v1/template-programmer/template/{templateId}, put /dna/intent/api/v1/template-programmer/template, + post /dna/intent/api/v1/template-programmer/project/name/exportprojects, + post /dna/intent/api/v1/template-programmer/template/exporttemplates, + post /dna/intent/api/v1/template-programmer/project/importprojects, + post /dna/intent/api/v1/template-programmer/project/name/{projectName}/template/importtemplates, """ EXAMPLES = r""" -- name: Create a new template +- name: Create a new template, export and import the project and template. cisco.dnac.template_intent: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" @@ -570,34 +1131,35 @@ dnac_log: True state: merged config: + - configuration_templates: author: string composite: true - createTime: 0 - customParamsOrder: true + create_time: 0 + custom_params_order: true description: string - deviceTypes: + device_types: - productFamily: string productSeries: string productType: string - failurePolicy: string + failure_policy: string id: string language: string - lastUpdateTime: 0 - latestVersionTime: 0 + last_update_time: 0 + latest_version_time: 0 name: string - parentTemplateId: string - projectId: string - projectName: string - projectDescription: string - rollbackTemplateContent: string - softwareType: string - softwareVariant: string - softwareVersion: string + parent_template_id: string + project_id: string + project_name: string + project_description: string + rollback_template_content: string + software_type: string + software_variant: string + software_version: string tags: - id: string name: string - templateContent: string - validationErrors: + template_content: string + validation_errors: rollbackTemplateErrors: - {} templateErrors: @@ -605,6 +1167,51 @@ templateId: string templateVersion: string version: string + export: + project: + - string + template: + - project_name : string + template_name: string + import: + project: + doVersion: true + export: + doVersion: true + payload: + - author: string + composite: true + containingTemplates: + - composite: true + description: string + device_types: + - productFamily: string + productSeries: string + productType: string + id: string + language: string + name: string + project_name: string + rollbackTemplateParams: + - binding: string + customOrder: 0 + dataType: string + defaultValue: string + description: string + displayName: string + group: string + id: string + instructionText: string + key: string + notParam: true + order: 0 + paramArray: true + parameterName: string + provider: string + range: + - id: string + project_name: string + """ @@ -653,6 +1260,29 @@ "response": {}, "msg": String } + +# Case_4: Given template list that needs to be exported +response_4: + description: Details of the templates in the list as returned by the Cisco DNAC Python SDK + returned: always + type: dict + sample: > + { + "response": {}, + "msg": String + } + +# Case_5: Given project list that needs to be exported +response_5: + description: Details of the projects in the list as returned by the Cisco DNAC Python SDK + returned: always + type: dict + sample: > + { + "response": {}, + "msg": String + } + """ from ansible.module_utils.basic import AnsibleModule @@ -673,6 +1303,8 @@ def __init__(self, module): self.have_template = {} self.supported_states = ["merged", "deleted"] self.accepted_languages = ["JINJA", "VELOCITY"] + self.export_template = [] + self.result['response'].append({}) def validate_input(self): """ @@ -680,17 +1312,18 @@ def validate_input(self): Checks the configuration provided in the playbook against a predefined specification to ensure it adheres to the expected structure and data types. Parameters: - - self: The instance of the class containing the 'config' attribute to be validated. + 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: - - self.msg: A message describing the validation result. - - self.status: The status of the validation (either 'success' or 'failed'). - - self.validated_config: If successful, a validated version of 'config' parameter. + The method returns an instance of the class with updated attributes: + - self.msg: A message describing the validation result. + - self.status: The status of the validation (either 'success' or 'failed'). + - self.validated_config: If successful, a validated version of 'config' parameter. Example: To use this method, create an instance of the class and call 'validate_input' on it. - If the validation succeeds, 'self.status' will be 'success' and 'self.validated_config' - will contain the validated configuration. If it fails, 'self.status' will be 'failed', - 'self.msg' will describe the validation issues. + If the validation succeeds, 'self.status' will be 'success' and 'self.validated_config' + will contain the validated configuration. If it fails, 'self.status' will be 'failed', + 'self.msg' will describe the validation issues. + """ if not self.config: @@ -698,37 +1331,105 @@ def validate_input(self): self.status = "success" return self - temp_spec = {'tags': {'type': 'list'}, - 'author': {'type': 'str'}, - 'composite': {'type': 'bool'}, - 'containingTemplates': {'type': 'list'}, - 'createTime': {'type': 'int'}, - 'customParamsOrder': {'type': 'bool'}, - 'description': {'type': 'str'}, - 'deviceTypes': {'type': 'list', 'elements': 'dict'}, - # 'deviceTypes': {'type': 'list', 'productFamily': {'type': 'list', 'elements': 'dict'}}, - 'failurePolicy': {'type': 'str'}, - 'id': {'type': 'str'}, - 'language': {'type': 'str'}, - 'lastUpdateTime': {'type': 'int'}, - 'latestVersionTime': {'type': 'int'}, - 'name': {'type': 'str'}, - 'parentTemplateId': {'type': 'str'}, - 'projectId': {'type': 'str'}, - 'projectName': {'required': True, 'type': 'str'}, - 'projectDescription': {'type': 'str'}, - 'rollbackTemplateContent': {'type': 'str'}, - 'rollbackTemplateParams': {'type': 'list'}, - 'softwareType': {'type': 'str'}, - 'softwareVariant': {'type': 'str'}, - 'softwareVersion': {'type': 'str'}, - 'templateContent': {'type': 'str'}, - 'templateParams': {'type': 'list'}, - 'templateName': {'type': 'str'}, - 'validationErrors': {'type': 'dict'}, - 'version': {'type': 'str'}, - 'versionDescription': {'type': 'str'} - } + temp_spec = { + "configuration_templates": { + 'type': 'dict', + 'tags': {'type': 'list'}, + 'author': {'type': 'str'}, + 'composite': {'type': 'bool'}, + 'containing_templates': {'type': 'list'}, + 'create_time': {'type': 'int'}, + 'custom_params_order': {'type': 'bool'}, + 'description': {'type': 'str'}, + 'device_types': { + 'type': 'list', + 'elements': 'dict', + 'productFamily': {'type': 'str'}, + 'productSeries': {'type': 'str'}, + 'productType': {'type': 'str'}, + }, + 'failure_policy': {'type': 'str'}, + 'id': {'type': 'str'}, + 'language': {'type': 'str'}, + 'last_update_time': {'type': 'int'}, + 'latest_version_time': {'type': 'int'}, + 'name': {'type': 'str'}, + 'parent_template_id': {'type': 'str'}, + 'project_id': {'type': 'str'}, + 'project_name': {'type': 'str'}, + 'project_description': {'type': 'str'}, + 'rollback_template_content': {'type': 'str'}, + 'rollback_template_params': {'type': 'list'}, + 'software_type': {'type': 'str'}, + 'software_variant': {'type': 'str'}, + 'software_version': {'type': 'str'}, + 'template_content': {'type': 'str'}, + 'template_params': {'type': 'list'}, + 'template_name': {'type': 'str'}, + 'validation_errors': {'type': 'dict'}, + 'version': {'type': 'str'}, + 'version_description': {'type': 'str'} + }, + 'export': { + 'type': 'dict', + 'project': {'type': 'list', 'elements': 'str'}, + 'template': { + 'type': 'list', + 'elements': 'dict', + 'project_name': {'type': 'str'}, + 'template_name': {'type': 'str'} + } + }, + 'import': { + 'type': 'dict', + 'project': { + 'type': 'dict', + 'do_version': {'type': 'str', 'default': 'False'}, + }, + 'template': { + 'type': 'dict', + 'do_version': {'type': 'str', 'default': 'False'}, + 'payload': { + 'type': 'list', + 'elements': 'dict', + 'tags': {'type': 'list'}, + 'author': {'type': 'str'}, + 'composite': {'type': 'bool'}, + 'containingTemplates': {'type': 'list'}, + 'createTime': {'type': 'int'}, + 'customParamsOrder': {'type': 'bool'}, + 'description': {'type': 'str'}, + 'device_types': { + 'type': 'list', + 'elements': 'dict', + 'productFamily': {'type': 'str'}, + 'productSeries': {'type': 'str'}, + 'productType': {'type': 'str'}, + }, + 'failurePolicy': {'type': 'str'}, + 'id': {'type': 'str'}, + 'language': {'type': 'str'}, + 'lastUpdateTime': {'type': 'int'}, + 'latestVersionTime': {'type': 'int'}, + 'name': {'type': 'str'}, + 'parentTemplateId': {'type': 'str'}, + 'projectId': {'type': 'str'}, + 'project_name': {'type': 'str'}, + 'projectDescription': {'type': 'str'}, + 'rollbackTemplateContent': {'type': 'str'}, + 'rollbackTemplateParams': {'type': 'list'}, + 'software_type': {'type': 'str'}, + 'software_variant': {'type': 'str'}, + 'softwareVersion': {'type': 'str'}, + 'template_content': {'type': 'str'}, + 'templateParams': {'type': 'list'}, + 'template_name': {'type': 'str'}, + 'validationErrors': {'type': 'dict'}, + 'version': {'type': 'str'} + } + } + } + } # Validate template params valid_temp, invalid_params = validate_list_of_dicts( self.config, temp_spec @@ -747,84 +1448,73 @@ def validate_input(self): def get_project_params(self, params): """ - Store project parameters from the playbook for template processing in Cisco DNA Center. + Store project parameters from the playbook for template processing in DNAC. Parameters: - - params (dict): A dictionary containing project parameters from the playbook. + params (dict) - Playbook details containing Project information. + Returns: - - dict: A dictionary containing project parameters. - Description: - This method extracts project parameters, such as 'name' and - 'description', from the playbook and structures them into a - dictionary. These parameters are intended for further processing - in Cisco DNA Center, particularly in template-related operations. + project_params (dict) - Organized Project parameters. """ - project_params = {"name": params.get("projectName"), - "description": params.get("projectDescription") + configuration_templates = params.get("configuration_templates") + project_params = {"name": configuration_templates.get("project_name"), + "description": configuration_templates.get("project_description") } return project_params def get_template_params(self, params): """ - Store template parameters from the playbook for template processing in Cisco DNA Center. + Store template parameters from the playbook for template processing in DNAC. Parameters: - - params (dict): A dictionary containing template parameters from the playbook. + params (dict) - Playbook details containing Template information. + Returns: - - dict: A dictionary containing template parameters. - Description: - This method extracts template parameters from the playbook and - structures them into a dictionary. These parameters are intended - for further processing in Cisco DNA Center, particularly in - template-related operations. + temp_params (dict) - Organized template parameters. """ + configuration_templates = params.get("configuration_templates") temp_params = { - "tags": params.get("template_tag"), - "author": params.get("author"), - "composite": params.get("composite"), - "containingTemplates": params.get("containingTemplates"), - "createTime": params.get("createTime"), - "customParamsOrder": params.get("customParamsOrder"), - "description": params.get("template_description"), - "deviceTypes": params.get("deviceTypes"), - "failurePolicy": params.get("failurePolicy"), - "id": params.get("templateId"), - "language": params.get("language").upper(), - "lastUpdateTime": params.get("lastUpdateTime"), - "latestVersionTime": params.get("latestVersionTime"), - "name": params.get("templateName"), - "parentTemplateId": params.get("parentTemplateId"), - "projectId": params.get("projectId"), - "projectName": params.get("projectName"), - "rollbackTemplateContent": params.get("rollbackTemplateContent"), - "rollbackTemplateParams": params.get("rollbackTemplateParams"), - "softwareType": params.get("softwareType"), - "softwareVariant": params.get("softwareVariant"), - "softwareVersion": params.get("softwareVersion"), - "templateContent": params.get("templateContent"), - "templateParams": params.get("templateParams"), - "validationErrors": params.get("validationErrors"), - "version": params.get("version"), - "project_id": params.get("projectId"), + "tags": configuration_templates.get("template_tag"), + "author": configuration_templates.get("author"), + "composite": configuration_templates.get("composite"), + "containingTemplates": configuration_templates.get("containing_templates"), + "createTime": configuration_templates.get("create_time"), + "customParamsOrder": configuration_templates.get("custom_params_order"), + "description": configuration_templates.get("template_description"), + "deviceTypes": configuration_templates.get("device_types"), + "failurePolicy": configuration_templates.get("failure_policy"), + "id": configuration_templates.get("templateId"), + "language": configuration_templates.get("language").upper(), + "lastUpdateTime": configuration_templates.get("last_update_time"), + "latestVersionTime": configuration_templates.get("latest_version_time"), + "name": configuration_templates.get("template_name"), + "parentTemplateId": configuration_templates.get("parent_template_id"), + "projectId": configuration_templates.get("project_id"), + "projectName": configuration_templates.get("project_name"), + "rollbackTemplateContent": configuration_templates.get("rollback_template_content"), + "rollbackTemplateParams": configuration_templates.get("rollback_template_params"), + "softwareType": configuration_templates.get("software_type"), + "softwareVariant": configuration_templates.get("software_variant"), + "softwareVersion": configuration_templates.get("software_version"), + "templateContent": configuration_templates.get("template_content"), + "templateParams": configuration_templates.get("template_params"), + "validationErrors": configuration_templates.get("validation_errors"), + "version": configuration_templates.get("version"), + "project_id": configuration_templates.get("project_id"), } return temp_params def get_template(self, config): """ - Get the template needed for updating or creation from Cisco DNA Center. + Get the template needed for updation or creation. Parameters: - - config (dict): A dictionary containing configuration information. + config (dict) - Playbook details containing Template information. + Returns: - - dict or None: A dictionary containing template details if - found, None otherwise. - Description: - This method retrieves the template needed for updating or - creation from Cisco DNA Center based on the provided configuration - information. It calls the 'get_template_details' function from the - 'configuration_templates' family of the Cisco DNA Center API. + result (dict) - Template details for the given template ID. """ result = None @@ -842,37 +1532,32 @@ def get_template(self, config): def get_have_project(self, config): """ - Get current project-related information from Cisco DNA Center. + Get the current project related information from DNAC. Parameters: - - config (dict): A dictionary containing configuration information. + config (dict) - Playbook details containing Project information. + Returns: - - list or None: A list of template details if the project is found, None otherwise. - - Description: - This method retrieves current project-related information from - Cisco DNA Center based on the provided configuration information. - It calls the 'get_project_details' method to check if the specified - project exists in Cisco DNA Center. If the project is found, the - method updates the 'have_project' attribute with relevant details, - including the project ID and deletability status. + template_available (list) - Current project information. """ have_project = {} - given_project_name = config.get("projectName") + given_projectName = config.get("configuration_templates").get("project_name") template_available = None # Check if project exists. - project_details = self.get_project_details(given_project_name) + project_details = self.get_project_details(given_projectName) # DNAC returns project details even if the substring matches. # Hence check the projectName retrieved from DNAC. if not (project_details and isinstance(project_details, list)): - self.log("Project: {0} not found, need to create new project in DNAC".format(given_project_name)) + self.log("Project: {0} not found, need to create new project in DNAC" + .format(given_projectName)) return None - fetched_project_name = project_details[0].get('name') - if fetched_project_name != given_project_name: - self.log("Project {0} provided is not exact match in DNAC DB".format(given_project_name)) + fetched_projectName = project_details[0].get('name') + if fetched_projectName != given_projectName: + self.log("Project {0} provided is not exact match in DNAC DB" + .format(given_projectName)) return None template_available = project_details[0].get('templates') @@ -885,30 +1570,18 @@ def get_have_project(self, config): def get_have_template(self, config, template_available): """ - Get current template-related information from Cisco DNA Center. + Get the current template related information from DNAC. Parameters: - - config (dict): A dictionary containing configuration information. - - template_available (list): A list of available templates in the project. + config (dict) - Playbook details containing Template information. + template_available (list) - Current project information. Returns: - Returns an instance of the class with updated attributes and status - indicating the success or failure of the template retrieval process. - - object: An instance of the class with updated attributes. - - Description: - This method retrieves current template-related information from - Cisco DNA Center based on the provided configuration information. It - checks if the specified template in the playbook is available in the - project by calling the 'gets_the_templates_available' method. If the - template is available, it further checks if the specified template - is committed by calling the 'get_template' method. The method - updates the 'have_template' attribute with relevant details, - including the template ID, commit status, and template information. + self """ - project_name = config.get("projectName") - template_name = config.get("templateName") + projectName = config.get("configuration_templates").get("project_name") + templateName = config.get("configuration_templates").get("template_name") template = None have_template = {} @@ -917,11 +1590,11 @@ def get_have_template(self, config, template_available): template_details = get_dict_result(template_available, "name", - template_name) + templateName) # Check if specified template in playbook is available if not template_details: - self.log("Template {0} not found in project {1}".format(template_name, project_name)) - self.msg = "Template : {0} missing, new template to be created".format(template_name) + self.log("Template {0} not found in project {1}".format(templateName, projectName)) + self.msg = "Template : {0} missing, new template to be created".format(templateName) self.status = "success" return self @@ -931,14 +1604,14 @@ def get_have_template(self, config, template_available): template_list = self.dnac_apply['exec']( family="configuration_templates", function="gets_the_templates_available", - params={"project_names": config.get("projectName")}, + params={"projectNames": config.get("projectName")}, ) have_template["isCommitPending"] = True # This check will fail if specified template is there not committed in dnac if template_list and isinstance(template_list, list): template_info = get_dict_result(template_list, "name", - template_name) + templateName) if template_info: template = self.get_template(config) have_template["template"] = template @@ -946,12 +1619,12 @@ def get_have_template(self, config, template_available): have_template["template_found"] = template is not None \ and isinstance(template, dict) self.log("Template {0} is found and template " - "details are :{1}".format(template_name, str(template))) + "details are :{1}".format(templateName, str(template))) # There are committed templates in the project but the # one specified in the playbook may not be committed self.log("Commit pending for template name {0}" - " is {1}".format(template_name, have_template.get('isCommitPending'))) + " is {1}".format(templateName, have_template.get('isCommitPending'))) self.have_template = have_template self.msg = "Successfully collected all template parameters from dnac for comparison" @@ -960,88 +1633,73 @@ def get_have_template(self, config, template_available): def get_have(self, config): """ - Get current project and template details from Cisco DNA Center. + Get the current project and template details from DNAC. Parameters: - - config (dict): A dictionary containing configuration information. + config (dict) - Playbook details containing Project/Template information. + Returns: - The method returns an instance of the class with updated attributes: - - self.msg: A message describing collecting the information. - - self.status: status of the retrieval (either 'success' or 'failed'). - - Description: - This method retrieves current project and template details from - Cisco DNA Center based on the provided configuration information. It - first calls the 'get_have_project' method to obtain information about - the current project. If the project is found, it then calls the - 'get_have_template' method to obtain information about the current - template in the project. + self """ - - template_available = self.get_have_project(config) - if template_available: - self.get_have_template(config, template_available) + configuration_templates = config.get("configuration_templates") + if configuration_templates: + if not configuration_templates.get("project_name"): + self.msg = "Mandatory Parameter project_name not available" + self.status = "failed" + return self + template_available = self.get_have_project(config) + if template_available: + self.get_have_template(config, template_available) self.msg = "Successfully collected all project and template \ parameters from dnac for comparison" self.status = "success" return self - def get_project_details(self, project_name): + def get_project_details(self, projectName): """ - Get details of a specific project from Cisco DNA Center. + Get the details of specific project name provided. Parameters: - - project_name (str): Name of the project for which details - are requested. + projectName (str) - Project Name + Returns: - - list or None: A list of project details if the project is - found, None otherwise. - Description: - This method retrieves details of a specific project from - Cisco DNA Center based on the provided project name. It - calls the 'get_projects' method from 'configuration_templates' - family of the Cisco DNA Center API. + items (dict) - Project details with given project name. """ items = self.dnac_apply['exec']( family="configuration_templates", function='get_projects', op_modifies=True, - params={"name": project_name}, + params={"name": projectName}, ) return items def get_want(self, config): """ - Get all template and project-related information from the playbook - that is needed to be created in Cisco DNA Center. + Get all the template and project related information from playbook + that is needed to be created in DNAC. Parameters: - - config (dict): A dictionary containing configuration information. + config (dict) - Playbook details. + Returns: - The method returns an instance of the class with updated attributes: - - self.msg: A message describing collecting the information. - - self.status: status of the retrieval (either 'success' or 'failed'). - - Description: - This method retrieves all template and project-related information - from the playbook based on the provided configuration information. - It calls the 'get_template_params' and 'get_project_params' methods - to obtain details about the template and project, respectively. + self """ want = {} - template_params = self.get_template_params(config) - project_params = self.get_project_params(config) - version_comments = config.get("versionDescription") + configuration_templates = config.get("configuration_templates") + if configuration_templates: + template_params = self.get_template_params(config) + project_params = self.get_project_params(config) + version_comments = configuration_templates.get("version_description") - if self.params.get("state") == "merged": - self.update_mandatory_parameters(template_params) + if self.params.get("state") == "merged": + self.update_mandatory_parameters(template_params) - want["template_params"] = template_params - want["project_params"] = project_params - want["comments"] = version_comments + want["template_params"] = template_params + want["project_params"] = project_params + want["comments"] = version_comments self.want = want self.msg = "Successfully collected all parameters from playbook " + \ @@ -1051,29 +1709,24 @@ def get_want(self, config): def create_project_or_template(self, is_create_project=False): """ - Call Cisco DNA Center API to create a project or template + Call DNAC API to create project or template based on the input provided. Parameters: - - is_create_project (bool, optional): Flag indicating whether - to create a project (True) or a template (False). - Defaults to False. + is_create_project (bool) - Default value is False. + Returns: - - tuple: A tuple containing the creation ID and a boolean indicating - if the creation was successful. The creation ID is the unique - identifier assigned to the created project or template. - Description: - This method calls the Cisco DNA Center API to create a project or - template based on the input parameters. It uses the 'create_project' - or 'create_template' function from the 'configuration_templates' - family of the Cisco DNA Center API. + creation_id (str) - Project Id. + created (str) - True if Project created, else False. """ creation_id = None created = False + self.log(str(self.want)) template_params = self.want.get("template_params") project_params = self.want.get("project_params") if is_create_project: + self.log("entered") params_key = project_params name = "project: {0}".format(project_params.get('name')) validation_string = "Successfully created project" @@ -1132,22 +1785,15 @@ def create_project_or_template(self, is_create_project=False): def requires_update(self): """ - Check if the template configuration provided requires an update. + Check if the template config given requires update. + + Parameters: + self - Current object. Returns: - - bool: True if the template configuration requires an update, - False otherwise. - Description: - This method compares the template parameters of the current - template ('have_template') and the requested template parameters - ('want') stored in the 'want' attribute. It checks for differences - in specified parameters, such as tags, author, composite, - containingTemplates, createTime, customParamsOrder, description, - deviceTypes, failurePolicy, id, language, lastUpdateTime, - latestVersionTime, name, parentTemplateId, projectId, projectName, - rollbackTemplateContent, rollbackTemplateParams, softwareType, - softwareVariant, softwareVersion, templateContent, templateParams, - validationErrors, and version. + bool - True if any parameter specified in obj_params differs between + current_obj and requested_obj, indicating that an update is required. + False if all specified parameters are equal. """ if self.have_template.get("isCommitPending"): @@ -1191,18 +1837,13 @@ def requires_update(self): def update_mandatory_parameters(self, template_params): """ - Update parameters that are mandatory for creating a template. + Update parameters which are mandatory for creating a template. Parameters: - - template_params (dict): A dictionary containing template parameters. + template_params (dict) - Template information. + Returns: - - None - Description: - This method updates the specified template parameters with - mandatory values required for creating a new template. It ensures - that the 'projectId', 'project_id', 'language', 'deviceTypes', and - 'softwareType' fields are properly set in the 'template_params' - dictionary. + None """ # Mandate fields required for creating a new template. @@ -1222,32 +1863,17 @@ def update_mandatory_parameters(self, template_params): def validate_input_merge(self, template_exists): """ - Validate input after obtaining all the parameters from Cisco DNA Center. + Validate input after getting all the parameters from DNAC. + "If mandate like deviceTypes, softwareType and language " + "already present in DNAC for a template." + "It is not required to be provided in playbook, " + "but if it is new creation error will be thrown to provide these fields. Parameters: - - template_exists (bool): Indicates whether the template already exists - in Cisco DNA Center. + template_exists (bool) - True if template exists, else False. + Returns: - - object: An instance of the class with updated attributes based on - validation: - - self.msg: A message describing the validation result. - - self.status: Status of the validation - ('success' or 'failed'). - Description: - This method validates the input parameters after retrieving them - from Cisco DNA Center. If the template already exists - ('template_exists' is True), it checks if mandatory parameters like - 'deviceTypes', 'softwareType', and 'language' are present in the - playbook. If not, it returns a failed status and a message indicating - that 'deviceTypes' and 'softwareType' are required arguments to - create templates. - If the template is a new creation, it verifies that the 'language' - is a valid value and sets it to 'JINJA' if not provided. It also - checks if 'deviceTypes' and 'softwareType' are present in the - playbook; if not, it returns a failed status and a message - indicating the missing fields. - The 'accepted_languages' attribute is assumed to be defined with a - list of accepted language values. + None """ template_params = self.want.get("template_params") @@ -1273,100 +1899,232 @@ def validate_input_merge(self, template_exists): self.status = "success" return self - def get_diff_merged(self, config): + def get_export_template_values(self, export_values): """ - Update/Create templates and projects in DNAC with fields - provided in Cisco DNA Center. + Get the export template values from the details provided by the playbook. + + Parameters: + export_values (bool) - All the template available under the project. - Paramenters: - - config (dict): A dictionary containing configuration - details from the playbook. Returns: - - object: An instance of the class with updated attributes based - on execution results: - - self.msg: A message describing the execution result. - - self.status: The status of the execution. - - self.result: A dictionary containing execution details, - including any changes made. - Description: - This method handles the process of updating or creating templates - and projects based on the provided configuration ('config') from the - playbook. It checks whether the project and template already exist - in Cisco DNA Center, and if not, it creates them. Then, it validates - the input parameters, checks if the template requires an update, and - performs the necessary actions such as updating the template and - versioning it. + self """ - is_project_found = self.have_project.get("project_found") - if not is_project_found: - project_id, project_created = self.create_project_or_template(is_create_project=True) - if project_created: - self.log("project created with projectId : {0}".format(project_id)) - else: + template_details = self.dnac._exec( + family="configuration_templates", + function='get_projects_details' + ) + for values in export_values: + self.log(str(values.get("projectName"))) + template_details = template_details.get("response") + all_template_details = get_dict_result(template_details, + "name", + values.get("projectName")) + self.log(str(all_template_details)) + all_template_details = all_template_details.get("templates") + self.log(str(all_template_details)) + template_detail = get_dict_result(all_template_details, + "name", + values.get("templateName")) + self.log(str(template_detail)) + if template_detail is None: + self.msg = "Invalid project_name and template_name in export" self.status = "failed" - self.msg = "Project creation failed" return self + self.export_template.append(template_detail.get("id")) - is_template_found = self.have_template.get("template_found") - template_params = self.want.get("template_params") - template_id = None - template_updated = False - self.validate_input_merge(is_template_found).check_return_status() - if is_template_found: - if self.requires_update(): - response = self.dnac_apply['exec']( - family="configuration_templates", - function="update_template", - params=template_params, - op_modifies=True, - ) - template_updated = True - template_id = self.have_template.get("id") - self.log("Updating Existing Template") - else: - # Template does not need update - self.result['response'] = self.have_template.get("template") - self.result['msg'] = "Template does not need update" - self.status = "exited" - return self + self.msg = "Successfully collected the export template IDs" + self.status = "success" + return self + + def export_project_or_tempalte(self, export): + """ + Export templates and projects in DNAC with fields provided in DNAC. + + Parameters: + export (dict) - Playbook details containing export information. + + Returns: + None + """ + + export_project = export.get("project") + self.log(str(export_project)) + if export_project: + response = self.dnac._exec( + family="configuration_templates", + function='export_projects', + params={"payload": export_project}, + ) + validation_string = "successfully exported project" + self.check_task_response_status(response, validation_string, True).check_return_status() + self.result['response'][0].update({"exportProject": self.msg}) + + export_values = export.get("template") + if export_values: + self.get_export_template_values(export_values).check_return_status() + self.log(str(self.export_template)) + response = self.dnac._exec( + family="configuration_templates", + function='export_templates', + params={"payload": self.export_template}, + ) + validation_string = "successfully exported template" + self.check_task_response_status(response, validation_string, True).check_return_status() + self.result['response'][0].update({"exportTemplate": self.msg}) + + def import_project_or_template(self, _import): + """ + Import templates and projects in DNAC with fields provided in DNAC. + + Parameters: + _import (dict) - Playbook details containing import information. + + Returns: + None + """ + + do_version = _import.get("project").get("do_version") + payload = None + if _import.get("project").get("payload"): + payload = _import.get("project").get("payload") else: - if template_params.get("name"): - template_id, template_updated = self.create_project_or_template() - else: - self.msg = "missing required arguments: TemplateName" - self.status = "failed" - return self + self.msg = "Mandatory parameter payload is not found under import project" + self.status = "failed" + return self + _import_project = { + "do_version": do_version, + # "payload": "{0}".format(payload) + "payload": payload + } + self.log(str(_import_project)) + if _import_project: + response = self.dnac._exec( + family="configuration_templates", + function='imports_the_projects_provided', + params=_import_project, + ) + validation_string = "successfully imported project" + self.check_task_response_status(response, validation_string).check_return_status() + self.result['response'][0].update({"importProject": validation_string}) - if template_updated: - # Template needs to be versioned - version_params = { - "comments": self.want.get("comments"), - "templateId": template_id - } - response = self.dnac_apply['exec']( + _import_template = _import.get("template") + if _import_template: + self.msg = "Mandatory paramter template is not found" + self.status = "failed" + return self + if _import_template.get("projectName"): + self.msg = "Mandatory paramter project_name is not found under import template" + self.status = "failed" + return self + if _import_template.get("payload"): + self.msg = "Mandatory paramter payload is not found under import template" + self.status = "failed" + return self + + self.log(str(_import_template)) + if _import_template: + response = self.dnac._exec( family="configuration_templates", - function="version_template", - op_modifies=True, - params=version_params + function='imports_the_templates_provided', + params=_import_template, ) - task_details = {} - task_id = response.get("response").get("taskId") - if not task_id: - self.msg = "Task id: {0} not found".format(task_id) - self.status = "failed" - return self - task_details = self.get_task_details(task_id) - self.result['changed'] = True - self.result['msg'] = task_details.get('progress') - self.result['diff'] = config - self.log(str(task_details)) - self.result['response'] = task_details if task_details else response + validation_string = "successfully imported template" + self.check_task_response_status(response, validation_string).check_return_status() + self.result['response'][0].update({"importTemplate": validation_string}) - if not self.result.get('msg'): - self.msg = "Error while versioning the template" - self.status = "failed" - return self + def get_diff_merged(self, config): + """ + Update/Create templates and projects in DNAC with fields provided in DNAC. + + Parameters: + config (dict) - Playbook details containing template information. + + Returns: + self + """ + + configuration_templates = config.get("configuration_templates") + if configuration_templates: + is_project_found = self.have_project.get("project_found") + if not is_project_found: + project_id, project_created = \ + self.create_project_or_template(is_create_project=True) + if project_created: + self.log("project created with projectId : {0}".format(project_id)) + else: + self.status = "failed" + self.msg = "Project creation failed" + return self + + is_template_found = self.have_template.get("template_found") + template_params = self.want.get("template_params") + template_id = None + template_updated = False + self.validate_input_merge(is_template_found).check_return_status() + if is_template_found: + if self.requires_update(): + response = self.dnac_apply['exec']( + family="configuration_templates", + function="update_template", + params=template_params, + op_modifies=True, + ) + template_updated = True + template_id = self.have_template.get("id") + self.log("Updating Existing Template") + else: + # Template does not need update + self.result['response'] = self.have_template.get("template") + self.result['msg'] = "Template does not need update" + self.status = "exited" + return self + else: + if template_params.get("name"): + template_id, template_updated = self.create_project_or_template() + else: + self.msg = "missing required arguments: template_name" + self.status = "failed" + return self + + if template_updated: + # Template needs to be versioned + version_params = { + "comments": self.want.get("comments"), + "templateId": template_id + } + response = self.dnac_apply['exec']( + family="configuration_templates", + function="version_template", + op_modifies=True, + params=version_params + ) + task_details = {} + task_id = response.get("response").get("taskId") + if not task_id: + self.msg = "Task id: {0} not found".format(task_id) + self.status = "failed" + return self + task_details = self.get_task_details(task_id) + self.result['changed'] = True + self.result['msg'] = task_details.get('progress') + self.result['diff'] = config.get("configuration_templates") + self.log(str(task_details)) + self.result['response'] = task_details if task_details else response + + if not self.result.get('msg'): + self.msg = "Error while versioning the template" + self.status = "failed" + return self + + export = config.get("export") + if export: + self.export_project_or_tempalte(export) + + _import = config.get("import") + if _import: + # _import_project = _import.get("project") + self.import_project_or_template(_import) self.msg = "Successfully completed merged state execution" self.status = "success" @@ -1374,33 +2132,20 @@ def get_diff_merged(self, config): def delete_project_or_template(self, config, is_delete_project=False): """ - Call Cisco DNA Center API to delete project or template - with provided inputs. + Call DNAC API to delete project or template with provided inputs. Parameters: - - config (dict): A dictionary containing configuration details - from the playbook. - - is_delete_project (bool, optional): Flag indicating whether - to delete a project (True) or a template (False). - Defaults to False. + config (dict) - Playbook details containing template information. + is_delete_project (bool) - True if we need to delete project, else False. + Returns: - - object: An instance of the class with updated attributes - based on execution results: - - self.msg: A message describing the execution result. - - self.status: The status of the execution - Description: - This method calls the Cisco DNA Center API to delete a project - or template based on the provided configuration ('config') from - the playbook. It checks whether the specified project or - template exists in DNAC and performs the necessary deletion. The - 'is_delete_project' flag determines whether a project or template - should be deleted. + self """ if is_delete_project: params_key = {"project_id": self.have_project.get("id")} deletion_value = "deletes_the_project" - name = "project: {0}".format(config.get('projectName')) + name = "project: {0}".format(config.get("configuration_templates").get('project_name')) else: template_params = self.want.get("template_params") params_key = {"template_id": self.have_template.get("id")} @@ -1417,7 +2162,7 @@ def delete_project_or_template(self, config, is_delete_project=False): task_details = self.get_task_details(task_id) self.result['changed'] = True self.result['msg'] = task_details.get('progress') - self.result['diff'] = config + self.result['diff'] = config.get("configuration_templates") self.log(str(task_details)) self.result['response'] = task_details if task_details else response @@ -1435,48 +2180,41 @@ def get_diff_deleted(self, config): Delete projects or templates in DNAC with fields provided in playbook. Parameters: - - config (dict): A dictionary containing configuration details - from the playbook. + config (dict) - Playbook details containing template information. + Returns: - - object: An instance of the class with updated attributes based - on execution results: - - self.msg: A message describing the execution result. - - self.status: The status of the execution ('success' or 'failed'). - Description: - This method handles the process of deleting projects or templates - based on the provided configuration ('config') from the playbook. It - checks whether the specified project and template exist in Cisco - DNA Center and performs the necessary deletion. If the template name - is empty, it deletes the entire project and its associated templates. + self """ - is_project_found = self.have_project.get("project_found") - projectName = config.get("projectName") + configuration_templates = config.get("configuration_templates") + if configuration_templates: + is_project_found = self.have_project.get("project_found") + projectName = config.get("configuration_templates").get("project_name") - if not is_project_found: - self.msg = "Project {0} is not found".format(projectName) - self.status = "failed" - return self - - is_template_found = self.have_template.get("template_found") - template_params = self.want.get("template_params") - template_name = config.get("templateName") - if template_params.get("name"): - if is_template_found: - self.delete_project_or_template(config) - else: - self.msg = "Invalid template {0} under project".format(template_name) + if not is_project_found: + self.msg = "Project {0} is not found".format(projectName) self.status = "failed" return self - else: - self.log("Template Name is empty, deleting the project and its associated templates") - is_project_deletable = self.have_project.get("isDeletable") - if is_project_deletable: - self.delete_project_or_template(config, is_delete_project=True) + + is_template_found = self.have_template.get("template_found") + template_params = self.want.get("template_params") + templateName = config.get("configuration_templates").get("template_name") + if template_params.get("name"): + if is_template_found: + self.delete_project_or_template(config) + else: + self.msg = "Invalid template {0} under project".format(templateName) + self.status = "failed" + return self else: - self.msg = "Project is not deletable" - self.status = "failed" - return self + self.log("Template Name is empty, deleting the project and associated templates") + is_project_deletable = self.have_project.get("isDeletable") + if is_project_deletable: + self.delete_project_or_template(config, is_delete_project=True) + else: + self.msg = "Project is not deletable" + self.status = "failed" + return self self.msg = "Successfully completed delete state execution" self.status = "success" @@ -1484,16 +2222,13 @@ def get_diff_deleted(self, config): def reset_values(self): """ - Reset all necessary attributes to default values. + Reset all neccessary attributes to default values. - Returns: - - None + Parameters: + self - The current object. - Description: - This method resets specific attributes of the class to their - default values. It clears the 'have_project', 'have_template', - and 'want' attributes, preparing the class for a new - operation or scenario. + Returns: + None """ self.have_project.clear() From 5c9d68206c9cc1546c5f33b53c2a764443f95caf Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 1 Dec 2023 00:03:13 +0530 Subject: [PATCH 11/50] Updated Snakes, solved bugs and Enhancement --- plugins/module_utils/dnac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index d4fb16cc0a..a95db74e63 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -196,7 +196,7 @@ def check_task_response_status(self, response, validation_string, data=False): if validation_string in task_details.get("progress").lower(): self.result['changed'] = True - if data == True: + if data is True: self.msg = task_details.get("data") self.status = "success" break From 4fbb2b090ef16f880ce200026282b7533c08a43d Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 1 Dec 2023 05:13:00 +0000 Subject: [PATCH 12/50] Resolving Sanity issues --- plugins/modules/discovery_intent.py | 34 +++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 659cf28e03..80052bfb38 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -373,23 +373,27 @@ def get_devices_list_info(self): self.result.update(dict(devices_info=devices_list)) return devices_list - def preprocessing_devices_info(self, devices_list: list): + def preprocessing_devices_info(self, devices_list: None): + if devices_list is None: + devices_list = [] ip_address_list = [device['ip'] for device in devices_list] if self.validated_config[0].get('discovery_type') == "SINGLE": ip_address_list = ip_address_list[0] else: ip_address_list = list( map( - lambda x: f'{x}-{x}', + lambda x: '{0}-{value}'.format(x, value=x), ip_address_list ) ) ip_address_list = ','.join(ip_address_list) return ip_address_list - def create_params(self, credential_ids: None, ip_address_list: str = ''): + def create_params(self, credential_ids=None, ip_address_list=None): if credential_ids is None: credential_ids = [] + if ip_address_list is None: + ip_address_list = '' new_object_params = {} new_object_params['cdpLevel'] = self.validated_config[0].get('cdp_level') new_object_params['discoveryType'] = self.validated_config[0].get('discovery_type') @@ -434,7 +438,11 @@ def create_params(self, credential_ids: None, ip_address_list: str = ''): new_object_params['userNameList'] = self.validated_config[0].get('user_name_list') return new_object_params - def create_discovery(self, credential_ids: list, ip_address_list: str): + def create_discovery(self, credential_ids=None, ip_address_list=None): + if credential_ids is None: + credential_ids = [] + if ip_address_list is None: + ip_address_list = '' result = self.dnac_apply['exec']( family="discovery", function="start_discovery", @@ -446,7 +454,9 @@ def create_discovery(self, credential_ids: list, ip_address_list: str): self.result.update(dict(discovery_result=result)) return result.response.get('taskId') - def get_task_status(self, task_id: str = None): + def get_task_status(self, task_id=None): + if task_id is None: + task_id = '' result = False params = dict(task_id=task_id) while True: @@ -514,7 +524,11 @@ def get_discoveries_by_range_until_success(self): self.result.update(dict(discovery_range=discovery)) return discovery - def get_discovery_device_info(self, discovery_id: str = None, task_id: str = None): + def get_discovery_device_info(self, discovery_id=None, task_id=None): + if discovery_id is None: + discovery_id = '' + if task_id is None: + task_id = '' params = dict( id=discovery_id, task_id=task_id, @@ -542,7 +556,7 @@ def get_discovery_device_info(self, discovery_id: str = None, task_id: str = Non time.sleep(3) if not result: - msg = f'Discovery network device with id {discovery_id} has not completed' + msg = 'Discovery network device with id {0} has not completed'.format(discovery_id) self.module.fail_json(msg=msg) self.result.update(dict(discovery_device_info=devices)) @@ -571,7 +585,8 @@ def get_diff_merged(self): self.get_dnac_global_credentials_v2_info() devices_list_info = self.get_devices_list_info() ip_address_list = self.preprocessing_devices_info(devices_list_info) - if exist_discovery := self.get_exist_discovery(): + exist_discovery = self.get_exist_discovery() + if exist_discovery: params = dict(id=exist_discovery.get('id')) discovery_task_id = self.delete_exist_discovery(params=params) complete_discovery = self.get_task_status(task_id=discovery_task_id) @@ -588,7 +603,8 @@ def get_diff_merged(self): return self def get_diff_deleted(self): - if exist_discovery := self.get_exist_discovery(): + exist_discovery = self.get_exist_discovery() + if exist_discovery: params = dict(id=exist_discovery.get('id')) discovery_task_id = self.delete_exist_discovery(params=params) complete_discovery = self.get_task_status(task_id=discovery_task_id) From 6853e22f9fb03e5cae3aded60e7f419caded014d Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 1 Dec 2023 05:19:57 +0000 Subject: [PATCH 13/50] Resolving Sanity issues2 --- plugins/modules/discovery_intent.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 80052bfb38..b33730b1d3 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -141,6 +141,7 @@ timeout: description: Time to wait for device response in seconds type: int + requirements: - dnacentersdk == 2.6.10 - python >= 3.5 @@ -373,7 +374,7 @@ def get_devices_list_info(self): self.result.update(dict(devices_info=devices_list)) return devices_list - def preprocessing_devices_info(self, devices_list: None): + def preprocessing_devices_info(self, devices_list=None): if devices_list is None: devices_list = [] ip_address_list = [device['ip'] for device in devices_list] From 9e0243e26cb22b43e30016de7232bd043f390633 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 1 Dec 2023 05:26:33 +0000 Subject: [PATCH 14/50] Resolving Sanity issues3 --- playbooks/discovery_intent.yml | 8 +------- plugins/modules/discovery_intent.py | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/playbooks/discovery_intent.yml b/playbooks/discovery_intent.yml index def89f084e..c8044e97a3 100644 --- a/playbooks/discovery_intent.yml +++ b/playbooks/discovery_intent.yml @@ -41,10 +41,4 @@ start_index: 1 records_to_return: 25 snmp_version: v2 - - register: discovery_task - - - name: Debug get discovery task - ansible.builtin.debug: - msg: "{{discovery_task}}" - + diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index b33730b1d3..2c4131019a 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -141,7 +141,6 @@ timeout: description: Time to wait for device response in seconds type: int - requirements: - dnacentersdk == 2.6.10 - python >= 3.5 From de42a1d4c62d36a041cb5ec61b2da5ddbabcdffb Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 1 Dec 2023 05:36:12 +0000 Subject: [PATCH 15/50] Resolving Sanity issues4 --- plugins/modules/discovery_intent.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index 2c4131019a..ab6cee0fde 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -288,7 +288,7 @@ def validate_input(self): self.msg = "config not available in playbook for validation" self.status = "success" return self - + default_dicovery_name = 'discovery_' + str(time.time()) discovery_spec = { 'cdp_level': {'type': 'int', 'required': False, 'default': 16}, @@ -306,7 +306,7 @@ def validate_input(self): 'lldp_level': {'type': 'int', 'required': False, 'default': 16}, 'discovery_name': {'type': 'dict', 'required': False, - 'default': 'discovery_{}'.format(time.time())}, + 'default': 'discovery_{}'.format(default_dicovery_name)}, 'netconf_port': {'type': 'str', 'required': False}, 'password_list': {'type': 'list', 'required': False, 'elements': 'str'}, @@ -373,7 +373,7 @@ def get_devices_list_info(self): self.result.update(dict(devices_info=devices_list)) return devices_list - def preprocessing_devices_info(self, devices_list=None): + def preprocessing_devices_info(self, devices_list: None): if devices_list is None: devices_list = [] ip_address_list = [device['ip'] for device in devices_list] From 840b0baaf8bd18b1dd869a4a534ceb3bc1d54bf4 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 1 Dec 2023 05:45:52 +0000 Subject: [PATCH 16/50] Resolving Sanity issues5 --- plugins/modules/discovery_intent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index ab6cee0fde..a4b9a2d4ba 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -306,7 +306,7 @@ def validate_input(self): 'lldp_level': {'type': 'int', 'required': False, 'default': 16}, 'discovery_name': {'type': 'dict', 'required': False, - 'default': 'discovery_{}'.format(default_dicovery_name)}, + 'default': '{0}'.format(default_dicovery_name)}, 'netconf_port': {'type': 'str', 'required': False}, 'password_list': {'type': 'list', 'required': False, 'elements': 'str'}, @@ -373,7 +373,7 @@ def get_devices_list_info(self): self.result.update(dict(devices_info=devices_list)) return devices_list - def preprocessing_devices_info(self, devices_list: None): + def preprocessing_devices_info(self, devices_list=None): if devices_list is None: devices_list = [] ip_address_list = [device['ip'] for device in devices_list] From d75937e42871afa84405e8977708024b532d8ada Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 1 Dec 2023 07:49:47 +0000 Subject: [PATCH 17/50] Making a few corrections --- plugins/modules/discovery_intent.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index a4b9a2d4ba..a8e6fe10d6 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -392,8 +392,6 @@ def preprocessing_devices_info(self, devices_list=None): def create_params(self, credential_ids=None, ip_address_list=None): if credential_ids is None: credential_ids = [] - if ip_address_list is None: - ip_address_list = '' new_object_params = {} new_object_params['cdpLevel'] = self.validated_config[0].get('cdp_level') new_object_params['discoveryType'] = self.validated_config[0].get('discovery_type') @@ -441,8 +439,6 @@ def create_params(self, credential_ids=None, ip_address_list=None): def create_discovery(self, credential_ids=None, ip_address_list=None): if credential_ids is None: credential_ids = [] - if ip_address_list is None: - ip_address_list = '' result = self.dnac_apply['exec']( family="discovery", function="start_discovery", @@ -455,8 +451,6 @@ def create_discovery(self, credential_ids=None, ip_address_list=None): return result.response.get('taskId') def get_task_status(self, task_id=None): - if task_id is None: - task_id = '' result = False params = dict(task_id=task_id) while True: @@ -525,10 +519,6 @@ def get_discoveries_by_range_until_success(self): return discovery def get_discovery_device_info(self, discovery_id=None, task_id=None): - if discovery_id is None: - discovery_id = '' - if task_id is None: - task_id = '' params = dict( id=discovery_id, task_id=task_id, From 5aede288e3f0db05388398dd39b8f7048e5fad08 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 1 Dec 2023 17:06:20 +0530 Subject: [PATCH 18/50] Distribute and activate image by fetching device based on site and fix swim issues --- playbooks/swim_intent.yml | 31 +- plugins/modules/swim_intent.py | 762 ++++++++++++++++++++++----------- 2 files changed, 540 insertions(+), 253 deletions(-) diff --git a/playbooks/swim_intent.yml b/playbooks/swim_intent.yml index 77a8c217e0..687a84e251 100644 --- a/playbooks/swim_intent.yml +++ b/playbooks/swim_intent.yml @@ -18,21 +18,26 @@ dnac_debug: "{{dnac_debug}}" dnac_log: true config: - - importImageDetails: + - import_image_details: type: "{{ item.type }}" - urlDetails: - payload: "{{ item.urlDetails.payload }}" - taggingDetails: - deviceRole: "{{ item.device_role }}" - deviceFamilyName: "{{ item.device_family_name }}" + url_details: + payload: "{{ item.url_details.payload }}" + tagging_details: + device_role: "{{ item.device_role }}" + device_family_name: "{{ item.device_family_name }}" + device_type: "{{item.device_type}}" tagging: true - imageDistributionDetails: - deviceSerialNumber: "{{ item.device_serial_number }}" - imageActivationDetails: - scehduleValidate: false - activateLowerImageVersion: true - deviceSerialNumber: "{{ item.device_serial_number }}" - distributeIfNeeded: true + image_distribution_details: + site_name: "{{item.site_name}}" + device_role: "{{ item.device_role }}" + device_family_name: "{{ item.device_family_name }}" + image_activation_details: + site_name: "{{item.site_name}}" + device_role: "{{ item.device_role }}" + device_family_name: "{{ item.device_family_name }}" + scehdule_validate: false + distribute_if_needed: true + with_items: "{{ image_details }}" tags: - swim \ No newline at end of file diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index e46fe43301..fed6633c8b 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -41,33 +41,33 @@ elements: dict required: True suboptions: - importImageDetails: + import_image_details: description: Details of image being imported type: dict suboptions: type: description: The source of import, supports url import or local import. type: str - localImageDetails: + local_image_details: description: Details of the local path of the image to be imported. type: dict suboptions: - filePath: + file_path: description: File absolute path. type: str - isThirdParty: + is_third_party: description: IsThirdParty query parameter. Third party Image check. type: bool - thirdPartyApplicationType: + third_party_application_type: description: ThirdPartyApplicationType query parameter. Third Party Application Type. type: str - thirdPartyImageFamily: + third_party_image_family: description: ThirdPartyImageFamily query parameter. Third Party image family. type: str - thirdPartyVendor: + third_party_vendor: description: ThirdPartyVendor query parameter. Third Party Vendor. type: str - urlDetails: + url_details: description: URL details for SWIM import type: dict suboptions: @@ -76,49 +76,49 @@ type: list elements: dict suboptions: - applicationType: + application_type: description: Swim Import Via Url's applicationType. type: str - imageFamily: + image_family: description: Swim Import Via Url's imageFamily. type: str - sourceURL: - description: Swim Import Via Url's sourceURL. + source_url: + description: Swim Import Image Via Url. type: str - thirdParty: + third_party: description: ThirdParty flag. type: bool vendor: description: Swim Import Via Url's vendor. type: str - scheduleAt: + schedule_at: description: ScheduleAt query parameter. Epoch Time (The number of milli-seconds since January 1 1970 UTC) at which the distribution should be scheduled (Optional). type: str - scheduleDesc: + schedule_desc: description: ScheduleDesc query parameter. Custom Description (Optional). type: str - scheduleOrigin: + schedule_origin: description: ScheduleOrigin query parameter. Originator of this call (Optional). type: str - imageName: - description: SWIM Image name. - type: str - taggingDetails: + tagging_details: description: Details for tagging or untagging an image as golden type: dict suboptions: - imageName: + image_name: description: SWIM image name which will be tagged or untagged as golden. type: str - deviceRole: + device_role: description: Device Role. Permissible Values ALL, UNKNOWN, ACCESS, BORDER ROUTER, DISTRIBUTION and CORE. type: str - deviceFamilyName: - description: Device family name + device_family_name: + description: Device family name(Eg Switches and Hubs) + type: str + device_type: + description: Type of the device (Eg Cisco Catalyst 9300 Switch) type: str - siteName: + site_name: description: Site name for which SWIM image will be tagged/untagged as golden. If not provided, SWIM image will be mapped to global site. type: str @@ -127,58 +127,78 @@ If True then the given image will be tagged as golden. If False then the given image will be un-tagged as golden. type: bool - imageDistributionDetails: + image_distribution_details: description: Details for SWIM image distribution. Device on which the image needs to distributed can be speciifed using any of the following parameters - deviceSerialNumber, deviceIPAddress, deviceHostname or deviceMacAddress. type: dict suboptions: - imageName: + device_role: + description: Device Role. Permissible Values ALL, UNKNOWN, ACCESS, BORDER ROUTER, + DISTRIBUTION and CORE. + type: str + device_family_name: + description: Device family name + type: str + site_name: + description: Used to get device details associated to this site. + type: str + image_name: description: SWIM image's name type: str - deviceSerialNumber: + device_serial_number: description: Device serial number where the image needs to be distributed type: str - deviceIPAddress: + device_ip_address: description: Device IP address where the image needs to be distributed type: str - deviceHostname: + device_hostname: description: Device hostname where the image needs to be distributed type: str - deviceMacAddress: + device_mac_address: description: Device MAC address where the image needs to be distributed type: str - imageActivationDetails: + image_activation_details: description: Details for SWIM image activation. Device on which the image needs to activated can be speciifed using any of the following parameters - deviceSerialNumber, deviceIPAddress, deviceHostname or deviceMacAddress. type: dict suboptions: - activateLowerImageVersion: + device_role: + description: Device Role. Permissible Values ALL, UNKNOWN, ACCESS, BORDER ROUTER, + DISTRIBUTION and CORE. + type: str + device_family_name: + description: Device family name + type: str + site_name: + description: Used to get device details associated to this site. + type: str + activate_lower_image_version: description: ActivateLowerImageVersion flag. type: bool - deviceUpgradeMode: + device_upgrade_mode: description: Swim Trigger Activation's deviceUpgradeMode. type: str distributeIfNeeded: description: DistributeIfNeeded flag. type: bool - imageName: + image_name: description: SWIM image's name type: str - deviceSerialNumber: + device_serial_number: description: Device serial number where the image needs to be activated type: str - deviceIPAddress: + device_ip_address: description: Device IP address where the image needs to be activated type: str - deviceHostname: + device_hostname: description: Device hostname where the image needs to be activated type: str - deviceMacAddress: + device_mac_address: description: Device MAC address where the image needs to be activated type: str - scheduleValidate: + schedule_validate: description: ScheduleValidate query parameter. ScheduleValidate, validates data before schedule (Optional). type: bool @@ -212,34 +232,90 @@ dnac_debug: "{{dnac_debug}}" dnac_log: True config: - - importImageDetails: + - import_image_details: type: string urlDetails: payload: - - sourceURL: string - isThirdParty: bool - imageFamily: string + - source_url: string + is_third_party: bool + image_family: string vendor: string - applicationType: string - scheduleAt: string - scheduleDesc: string - scheduleOrigin: string - imageName: string - taggingDetails: - imageName: string - deviceRole: string - deviceFamilyName: string - siteName: string + application_type: string + schedule_at: string + schedule_desc: string + schedule_origin: string + tagging_details: + image_name: string + device_role: string + device_family_name: string + site_name: string + tagging: bool + image_distribution_details: + image_name: string + device_serial_number: string + image_activation_details: + schedule_validate: bool + activate_lower_image_version: bool + distribute_if_needed: bool + device_serial_number: string + image_name: string + +- name: Tag the given image as golden and load it on device + cisco.dnac.swim_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + config: + - tagging_details: + image_name: string + device_role: string + device_family_name: string + device_type: string + site_name: string tagging: bool - imageDistributionDetails: - imageName: string - deviceSerialNumber: string - imageActivationDetails: - scheduleValidate: bool - activateLowerImageVersion: bool - distributeIfNeeded: bool - deviceSerialNumber: string - imageName: string + +- name: Distribute the given image on devices associated to that site with specified role. + cisco.dnac.swim_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + config: + - image_distribution_details: + image_name: string + site_name: string + device_role: string + device_family_name: string + +- name: Activate the given image on devices associated to that site with specified role. + cisco.dnac.swim_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + config: + - image_activation_details: + image_name: string + site_name: string + device_role: string + device_family_name: string + scehdule_validate: bool + activate_lower_image_version: bool + distribute_if_needed: bool + """ RETURN = r""" @@ -294,11 +370,10 @@ def validate_input(self): return self temp_spec = dict( - importImageDetails=dict(type='dict'), - taggingDetails=dict(type='dict'), - imageDistributionDetails=dict(type='dict'), - imageActivationDetails=dict(type='dict'), - imageName=dict(type=str), + import_image_details=dict(type='dict'), + tagging_details=dict(type='dict'), + image_distribution_details=dict(type='dict'), + image_activation_details=dict(type='dict'), ) # Validate swim params @@ -316,12 +391,12 @@ def validate_input(self): self.status = "success" return self - def site_exists(self): + def site_exists(self, site_name): """ Parameters: - - self: The reference to the class instance. + self (object): An instance of a class used for interacting with Cisco DNA Center. Returns: - - tuple: A tuple containing two values: + tuple: A tuple containing two values: - site_exists (bool): A boolean indicating whether the site exists (True) or not (False). - site_id (str or None): The ID of the site if it exists, or None if the site is not found. Description: @@ -337,7 +412,7 @@ def site_exists(self): response = self.dnac._exec( family="sites", function='get_site', - params={"name": self.want.get('tagging_details').get('siteName')}, + params={"name": site_name}, ) except Exception as e: self.module.fail_json(msg="Site not found") @@ -355,6 +430,7 @@ def get_image_id(self, name): """ Retrieve the unique image ID based on the provided image name. Parameters: + self (object): An instance of a class used for interacting with Cisco DNA Center. name (str): The name of the software image to search for. Returns: str: The unique image ID (UUID) corresponding to the given image name. @@ -388,6 +464,7 @@ def is_image_exist(self, name): """ Retrieve the unique image ID based on the provided image name. Parameters: + self (object): An instance of a class used for interacting with Cisco DNA Center. name (str): The name of the software image to search for. Returns: str: The unique image ID (UUID) corresponding to the given image name. @@ -398,6 +475,7 @@ def is_image_exist(self, name): It extracts and returns the image ID if a single matching image is found. If no image or multiple images are found with the same name, it raises an exception. """ + image_exist = False image_response = self.dnac._exec( family="software_image_management_swim", @@ -415,17 +493,16 @@ def get_device_id(self, params): """ Retrieve the unique device ID based on the provided parameters. Parameters: + self (object): An instance of a class used for interacting with Cisco DNA Center. params (dict): A dictionary containing parameters to filter devices. Returns: str: The unique device ID corresponding to the filtered device. - Raises: - AnsibleFailJson: If the device is not found in the response. Description: This function sends a request to Cisco DNA Center to retrieve a list of devices based on the provided filtering parameters. If a single matching device is found, it extracts and returns the device ID. If no device or multiple devices match the criteria, it raises an exception. """ - + device_id = None response = self.dnac._exec( family="devices", function='get_device_list', @@ -438,14 +515,58 @@ def get_device_id(self, params): device_id = device_list[0].get("id") log("Device Id: " + str(device_id)) else: - self.module.fail_json(msg="Device not found", response=response) + self.log("Device not found") return device_id + def get_device_uuids(self, site_name, device_family, device_role): + """ + Retrieve a list of device UUIDs based on the specified criteria. + Parameters: + self (object): An instance of a class used for interacting with Cisco DNA Center. + site_name (str): The name of the site for which device UUIDs are requested. + device_family (str): The family/type of devices to filter on. + device_role (str): The role of devices to filter on. If None, all roles are considered. + Returns: + list: A list of device UUIDs that match the specified criteria. + Description: + The function checks the reachability status and role of devices in the given site. + Only devices with "Reachable" status are considered, and filtering is based on the specified + device family and role (if provided). + """ + + device_uuid_list = [] + if site_name: + site_exists = False + (site_exists, site_id) = self.site_exists(site_name) + if site_exists: + site_params = { + "site_id": site_id, + "device_family": device_family + } + response = self.dnac._exec( + family="sites", + function='get_membership', + op_modifies=True, + params=site_params, + ) + response = response['device'][0]['response'] + if len(response) > 0: + for item in response: + if item["reachabilityStatus"] != "Reachable": + continue + if "role" in item and device_role is not None and item["role"] == device_role.upper(): + device_uuid_list.append(item["instanceUuid"]) + elif device_role is None or device_role.upper() == "ALL": + device_uuid_list.append(item["instanceUuid"]) + + return device_uuid_list + def get_device_family_identifier(self, family_name): """ Retrieve and store the device family identifier based on the provided family name. Parameters: + self (object): An instance of a class used for interacting with Cisco DNA Center. family_name (str): The name of the device family for which to retrieve the identifier. Returns: None @@ -490,8 +611,9 @@ def get_have(self): if self.want.get("tagging_details"): have = {} tagging_details = self.want.get("tagging_details") - if tagging_details.get("imageName"): - image_id = self.get_image_id(tagging_details.get("imageName")) + if tagging_details.get("image_name"): + name = tagging_details.get("image_name").split("/")[-1] + image_id = self.get_image_id(name) have["tagging_image_id"] = image_id elif self.have.get("imported_image_id"): @@ -502,10 +624,10 @@ def get_have(self): # check if given site exists, store siteid # if not then use global site - site_name = tagging_details.get("siteName") + site_name = tagging_details.get("site_name") if site_name: site_exists = False - (site_exists, site_id) = self.site_exists() + (site_exists, site_id) = self.site_exists(site_name) if site_exists: have["site_id"] = site_id log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) @@ -516,15 +638,16 @@ def get_have(self): self.have.update(have) # check if given device family name exists, store indentifier value - family_name = tagging_details.get("deviceFamilyName") + family_name = tagging_details.get("device_type") self.get_device_family_identifier(family_name) if self.want.get("distribution_details"): have = {} distribution_details = self.want.get("distribution_details") # check if image for distributon is available - if distribution_details.get("imageName"): - image_id = self.get_image_id(distribution_details.get("imageName")) + if distribution_details.get("image_name"): + name = distribution_details.get("image_name").split("/")[-1] + image_id = self.get_image_id(name) have["distribution_image_id"] = image_id elif self.have.get("imported_image_id"): @@ -534,21 +657,23 @@ def get_have(self): self.module.fail_json(msg="Image details for distribution not provided", response=[]) device_params = dict( - hostname=distribution_details.get("deviceHostname"), - serial_number=distribution_details.get("deviceSerialNumber"), - management_ip_address=distribution_details.get("deviceIPAddress"), - mac_address=distribution_details.get("deviceMacAddress"), + hostname=distribution_details.get("device_hostname"), + serial_number=distribution_details.get("device_serial_number"), + management_ip_address=distribution_details.get("device_ip_address"), + mac_address=distribution_details.get("device_mac_address"), ) device_id = self.get_device_id(device_params) - have["distribution_device_id"] = device_id + if device_id is not None: + have["distribution_device_id"] = device_id self.have.update(have) if self.want.get("activation_details"): have = {} activation_details = self.want.get("activation_details") # check if image for activation is available - if activation_details.get("imageName"): - image_id = self.get_image_id(activation_details.get("imageName")) + if activation_details.get("image_name"): + name = activation_details.get("image_name").split("/")[-1] + image_id = self.get_image_id(name) have["activation_image_id"] = image_id elif self.have.get("imported_image_id"): @@ -557,14 +682,23 @@ def get_have(self): else: self.module.fail_json(msg="Image details for activation not provided", response=[]) + site_name = activation_details.get("site_name") + if site_name: + site_exists = False + (site_exists, site_id) = self.site_exists(site_name) + if site_exists: + have["site_id"] = site_id + log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) + device_params = dict( - hostname=activation_details.get("deviceHostname"), - serial_number=activation_details.get("deviceSerialNumber"), - management_ip_address=activation_details.get("deviceIPAddress"), - mac_address=activation_details.get("deviceMacAddress"), + hostname=activation_details.get("device_hostname"), + serial_number=activation_details.get("device_serial_number"), + management_ip_address=activation_details.get("device_ip_address"), + mac_address=activation_details.get("device_mac_address"), ) device_id = self.get_device_id(device_params) - have["activation_device_id"] = device_id + if device_id is not None: + have["activation_device_id"] = device_id self.have.update(have) return self @@ -573,6 +707,7 @@ def get_want(self, config): """ Retrieve and store import, tagging, distribution, and activation details from playbook configuration. Parameters: + self (object): An instance of a class used for interacting with Cisco DNA Center. config (dict): The configuration dictionary containing image import and other details. Returns: self: The current instance of the class with updated 'want' attributes. @@ -585,19 +720,19 @@ def get_want(self, config): """ want = {} - if config.get("importImageDetails"): + if config.get("import_image_details"): want["import_image"] = True - want["import_type"] = config.get("importImageDetails").get("type").lower() + want["import_type"] = config.get("import_image_details").get("type").lower() if want["import_type"] == "url": - want["url_import_details"] = config.get("importImageDetails").get("urlDetails") + want["url_import_details"] = config.get("import_image_details").get("url_details") elif want["import_type"] == "local": - want["local_import_details"] = config.get("importImageDetails").get("localImageDetails") + want["local_import_details"] = config.get("import_image_details").get("local_image_details") else: self.module.fail_json(msg="Incorrect import type. Supported Values: local or url") - want["tagging_details"] = config.get("taggingDetails") - want["distribution_details"] = config.get("imageDistributionDetails") - want["activation_details"] = config.get("imageActivationDetails") + want["tagging_details"] = config.get("tagging_details") + want["distribution_details"] = config.get("image_distribution_details") + want["activation_details"] = config.get("image_activation_details") self.want = want log(str(self.want)) @@ -608,9 +743,9 @@ def get_diff_import(self): """ Check the image import type and fetch the image ID for the imported image for further use. Parameters: - None + self (object): An instance of a class used for interacting with Cisco DNA Center. Returns: - self: The current instance of the class with updated 'have' attributes. + self (object): An instance of a class used for interacting with Cisco DNA Center. Description: This function checks the type of image import (URL or local) and proceeds with the import operation accordingly. It then monitors the import task's progress and updates the 'result' dictionary. If the operation is successful, @@ -619,100 +754,109 @@ def get_diff_import(self): imported image and stores it in the 'have' dictionary for later use. """ - if not self.want.get("import_image"): - image_name = self.want.get("image_name") - image_id = self.get_image_id(image_name) - self.have["imported_image_id"] = image_id - return self - - if self.want.get("import_type") == "url": - image_name = self.want.get("url_import_details").get("payload")[0].get("sourceURL") - - # Code to check if image already exist in the DNAC - name = image_name.split('/')[-1] - image_exist = self.is_image_exist(name) - if image_exist: - image_id = self.get_image_id(image_name) - self.have["imported_image_id"] = image_id - log_msg = "Image {0} already exists in the Cisco DNA Center".format(name) - self.result['msg'] = log_msg - log(log_msg) - self.result['changed'] = False - return self - - url_import_params = dict( - payload=self.want.get("url_import_details").get("payload"), - schedule_at=self.want.get("url_import_details").get("scheduleAt"), - schedule_desc=self.want.get("url_import_details").get("scheduleDesc"), - schedule_origin=self.want.get("url_import_details").get("scheduleOrigin"), - ) - response = self.dnac._exec( - family="software_image_management_swim", - function='import_software_image_via_url', - op_modifies=True, - params=url_import_params, - ) - else: - image_name = self.want.get("local_import_details").get("filePath") - # Code to check if image already exist in the DNAC - name = image_name.split('/')[-1] - image_exist = self.is_image_exist(name) - if image_exist: - image_id = self.get_image_id(name) - self.have["imported_image_id"] = image_id - log_msg = "Image {0} already exists in the Cisco DNA Center".format(name) - self.result['msg'] = log_msg - log(log_msg) - self.result['changed'] = False - return self - - local_import_params = dict( - is_third_party=self.want.get("local_import_details").get("isThirdParty"), - third_party_vendor=self.want.get("local_import_details").get("thirdPartyVendor"), - third_party_image_family=self.want.get("local_import_details").get("thirdPartyImageFamily"), - third_party_application_type=self.want.get("local_import_details").get("thirdPartyApplicationType"), - file_path=self.want.get("local_import_details").get("filePath"), - ) - response = self.dnac._exec( - family="software_image_management_swim", - function='import_local_software_image', - op_modifies=True, - params=local_import_params, - file_paths=[('file_path', 'file')], - ) - - log(str(response)) + try: + if self.want.get("import_type") == "url": + image_name = self.want.get("url_import_details").get("payload")[0].get("source_url") + + # Code to check if image already exist in the DNAC + name = image_name.split('/')[-1] + image_exist = self.is_image_exist(name) + if image_exist: + image_id = self.get_image_id(name) + self.have["imported_image_id"] = image_id + log_msg = "Image {0} already exists in the Cisco DNA Center".format(name) + self.result['msg'] = log_msg + log(log_msg) + self.status = "success" + self.result['changed'] = False + return self + + url_import_params = dict( + payload=self.want.get("url_import_details").get("payload"), + schedule_at=self.want.get("url_import_details").get("schedule_at"), + schedule_desc=self.want.get("url_import_details").get("schedule_desc"), + schedule_origin=self.want.get("url_import_details").get("schedule_origin"), + ) + response = self.dnac._exec( + family="software_image_management_swim", + function='import_software_image_via_url', + op_modifies=True, + params=url_import_params, + ) + else: + image_name = self.want.get("local_import_details").get("filePath") + # Code to check if image already exist in the DNAC + name = image_name.split('/')[-1] + image_exist = self.is_image_exist(name) + if image_exist: + image_id = self.get_image_id(name) + self.have["imported_image_id"] = image_id + log_msg = "Image {0} already exists in the Cisco DNA Center".format(name) + self.result['msg'] = log_msg + log(log_msg) + self.status = "success" + self.result['changed'] = False + return self + + local_import_params = dict( + is_third_party=self.want.get("local_import_details").get("is_third_party"), + third_party_vendor=self.want.get("local_import_details").get("third_party_vendor"), + third_party_image_family=self.want.get("local_import_details").get("third_party_image_family"), + third_party_application_type=self.want.get("local_import_details").get("third_party_application_type"), + file_path=self.want.get("local_import_details").get("file_path"), + ) + response = self.dnac._exec( + family="software_image_management_swim", + function='import_local_software_image', + op_modifies=True, + params=local_import_params, + file_paths=[('file_path', 'file')], + ) - task_details = {} - task_id = response.get("response").get("taskId") - while (True): - task_details = self.get_task_details(task_id) - name = image_name.split('/')[-1] - if task_details and \ - ("completed successfully" in task_details.get("progress").lower()): - self.result['changed'] = True - log_msg = "Swim Image {0} imported successfully".format(name) - self.result['msg'] = log_msg - log(log_msg) - break + log(str(response)) - if task_details and task_details.get("isError"): - if "already exists" in task_details.get("failureReason"): - log_msg = "SWIM Image {0} already exists in the Cisco DNA Center".format(name) + task_details = {} + task_id = response.get("response").get("taskId") + while (True): + task_details = self.get_task_details(task_id) + name = image_name.split('/')[-1] + if task_details and \ + ("completed successfully" in task_details.get("progress").lower()): + self.result['changed'] = True + self.status = "success" + log_msg = "Swim Image {0} imported successfully".format(name) self.result['msg'] = log_msg log(log_msg) - self.result['changed'] = False break - else: - self.module.fail_json(msg=task_details.get("failureReason"), - response=task_details) - self.result['response'] = task_details if task_details else response + if task_details and task_details.get("isError"): + if "already exists" in task_details.get("failureReason"): + log_msg = "SWIM Image {0} already exists in the Cisco DNA Center".format(name) + self.result['msg'] = log_msg + log(log_msg) + self.status = "success" + self.result['changed'] = False + break + else: + self.status = "failed" + self.msg = task_details.get("failureReason") + self.result[response] = task_details + return self - # Fetch image_id for the imported image for further use - image_name = image_name.split('/')[-1] - image_id = self.get_image_id(image_name) - self.have["imported_image_id"] = image_id + self.result['response'] = task_details if task_details else response + + # Fetch image_id for the imported image for further use + image_name = image_name.split('/')[-1] + image_id = self.get_image_id(image_name) + self.have["imported_image_id"] = image_id + + return self + + except Exception as e: + self.log("Import Image details are not given in the playbook") + log("Import Image details are not given in the playbook without self") + self.status = "success" + self.result['changed'] = False return self @@ -720,9 +864,9 @@ def get_diff_tagging(self): """ Tag or untag a software image as golden based on provided tagging details. Parameters: - None + self (object): An instance of a class used for interacting with Cisco DNA Center. Returns: - None + self (object): An instance of a class used for interacting with Cisco DNA Center. Description: This function tags or untags a software image as a golden image in Cisco DNAC based on the provided tagging details. The tagging action is determined by the value of the 'tagging' attribute @@ -739,7 +883,7 @@ def get_diff_tagging(self): imageId=self.have.get("tagging_image_id"), siteId=self.have.get("site_id"), deviceFamilyIdentifier=self.have.get("device_family_identifier"), - deviceRole=tagging_details.get("deviceRole") + deviceRole=tagging_details.get("device_role") ) log("Image params for tagging image as golden:" + str(image_params)) @@ -749,6 +893,7 @@ def get_diff_tagging(self): op_modifies=True, params=image_params ) + log(str(response)) else: image_params = dict( @@ -765,6 +910,7 @@ def get_diff_tagging(self): op_modifies=True, params=image_params ) + log(str(response)) if response: task_details = {} @@ -773,16 +919,19 @@ def get_diff_tagging(self): if not task_details.get("isError"): self.result['changed'] = True self.result['msg'] = task_details.get("progress") + self.status = "success" self.result['response'] = task_details if task_details else response + return self + def get_diff_distribution(self): """ Get image distribution parameters from the playbook and trigger image distribution. Parameters: - None + self (object): An instance of a class used for interacting with Cisco DNA Center. Returns: - None + self (object): An instance of a class used for interacting with Cisco DNA Center. Description: This function retrieves image distribution parameters from the playbook's 'distribution_details' and triggers the distribution of the specified software image to the specified device. It monitors the distribution task's @@ -790,45 +939,106 @@ def get_diff_distribution(self): """ distribution_details = self.want.get("distribution_details") - distribution_params = dict( - payload=[dict( - deviceUuid=self.have.get("distribution_device_id"), - imageUuid=self.have.get("distribution_image_id") - )] - ) - log("Distribution Params: " + str(distribution_params)) + site_name = distribution_details.get("site_name") + device_family = distribution_details.get("device_family_name") + device_role = distribution_details.get("device_role") + device_uuid_list = self.get_device_uuids(site_name, device_family, device_role) + image_id = self.have.get("distribution_image_id") + + if len(device_uuid_list) > 0: + self.log("List of device UUID's " + str(device_uuid_list)) + device_distribution_count = 0 + for device_uuid in device_uuid_list: + distribution_params = dict( + payload=[dict( + deviceUuid=device_uuid, + imageUuid=image_id + )] + ) + log("Distribution Params: " + str(distribution_params)) + response = self.dnac._exec( + family="software_image_management_swim", + function='trigger_software_image_distribution', + op_modifies=True, + params=distribution_params, + ) + if response: + task_details = {} + task_id = response.get("response").get("taskId") + while (True): + task_details = self.get_task_details(task_id) + if not task_details.get("isError") and \ + ("completed successfully" in task_details.get("progress")): + self.result['changed'] = True + self.status = "success" + self.result['msg'] = "Image with Id {0} Distributed Successfully".format(image_id) + device_distribution_count += 1 + break + + if task_details.get("isError"): + error_msg = "Image with Id {0} Distribution Failed".format(image_id) + self.result['response'] = task_details + break + + if device_distribution_count == 0: + self.status = "failed" + error_msg = "Image with Id {0} Distribution Failed for all devices".format(image_id) + elif device_distribution_count == len(device_uuid_list): + self.result['changed'] = True + self.status = "success" + self.result['msg'] = "Image with Id {0} Distributed Successfully for all devices".format(image_id) + else: + self.result['changed'] = True + self.status = "success" + self.result['msg'] = "Image with Id {0} Distributed and partially Successfull".format(image_id) - response = self.dnac._exec( - family="software_image_management_swim", - function='trigger_software_image_distribution', - op_modifies=True, - params=distribution_params, - ) - if response: - task_details = {} - task_id = response.get("response").get("taskId") - while (True): - task_details = self.get_task_details(task_id) - if not task_details.get("isError") and \ - ("completed successfully" in task_details.get("progress")): - self.result['changed'] = True - self.result['msg'] = "Image with Id {0} Distributed Successfully".format(self.have.get("distribution_image_id")) - break + return self - if task_details.get("isError"): - error_msg = "Image with Id {0} Distribution Failed".format(self.have.get("distribution_image_id")) - self.module.fail_json(msg=error_msg, - response=task_details) + if self.have.get("distribution_device_id"): + distribution_params = dict( + payload=[dict( + deviceUuid=self.have.get("distribution_device_id"), + imageUuid=image_id + )] + ) + log("Distribution Params: " + str(distribution_params)) - self.result['response'] = task_details if task_details else response + response = self.dnac._exec( + family="software_image_management_swim", + function='trigger_software_image_distribution', + op_modifies=True, + params=distribution_params, + ) + if response: + task_details = {} + task_id = response.get("response").get("taskId") + while (True): + task_details = self.get_task_details(task_id) + if not task_details.get("isError") and \ + ("completed successfully" in task_details.get("progress")): + self.result['changed'] = True + self.status = "success" + self.result['msg'] = "Image with Id {0} Distributed Successfully".format(image_id) + break + + if task_details.get("isError"): + error_msg = "Image with Id {0} Distribution Failed".format(image_id) + self.status = "failed" + self.result['response'] = task_details + self.msg = error_msg + return self + + self.result['response'] = task_details if task_details else response + + return self def get_diff_activation(self): """ Get image activation parameters from the playbook and trigger image activation. Parameters: - None + self (object): An instance of a class used for interacting with Cisco DNA Center. Returns: - None + self (object): An instance of a class used for interacting with Cisco DNA Center. Description: This function retrieves image activation parameters from the playbook's 'activation_details' and triggers the activation of the specified software image on the specified device. It monitors the activation task's progress and @@ -836,16 +1046,80 @@ def get_diff_activation(self): """ activation_details = self.want.get("activation_details") + site_name = activation_details.get("site_name") + device_family = activation_details.get("device_family_name") + device_role = activation_details.get("device_role") + device_uuid_list = self.get_device_uuids(site_name, device_family, device_role) + image_id = self.have.get("activation_image_id") + + if len(device_uuid_list) > 0: + self.log("List of device UUID's " + str(device_uuid_list)) + device_activation_count = 0 + for device_uuid in device_uuid_list: + payload = [dict( + activateLowerImageVersion=activation_details.get("activate_lower_image_version"), + deviceUpgradeMode=activation_details.get("device_upgrade_mode"), + distributeIfNeeded=activation_details.get("distribute_if_needed"), + deviceUuid=device_uuid, + imageUuidList=[image_id] + )] + + activation_params = dict( + schedule_validate=activation_details.get("scehdule_validate"), + payload=payload + ) + log("Activation Params: " + str(activation_params)) + + response = self.dnac._exec( + family="software_image_management_swim", + function='trigger_software_image_activation', + op_modifies=True, + params=activation_params, + ) + if response: + task_details = {} + task_id = response.get("response").get("taskId") + while (True): + task_details = self.get_task_details(task_id) + if not task_details.get("isError") and \ + ("completed successfully" in task_details.get("progress")): + self.result['changed'] = True + self.status = "success" + self.result['msg'] = "Image with Id {0} Activated Successfully".format(image_id) + device_activation_count += 1 + break + + if task_details.get("isError"): + error_msg = "Image with Id {0} Activation Failed".format(image_id) + self.result['response'] = task_details + break + + if device_activation_count == 0: + self.status = "failed" + msg = "Image with Id {0} Activation Failed for all devices".format(image_id) + elif device_activation_count == len(device_uuid_list): + self.result['changed'] = True + self.status = "success" + msg = "Image with Id {0} Activated Successfully for all devices".format(image_id) + else: + self.result['changed'] = True + self.status = "success" + msg = "Image with Id {0} Activated and partially Successfull".format(image_id) + self.result['msg'] = msg + self.log(msg) + + return self + payload = [dict( - activateLowerImageVersion=activation_details.get("activateLowerImageVersion"), - deviceUpgradeMode=activation_details.get("deviceUpgradeMode"), - distributeIfNeeded=activation_details.get("distributeIfNeeded"), + activateLowerImageVersion=activation_details.get("activate_lower_image_version"), + deviceUpgradeMode=activation_details.get("device_upgrade_mode"), + distributeIfNeeded=activation_details.get("distribute_if_needed"), deviceUuid=self.have.get("activation_device_id"), - imageUuidList=[self.have.get("activation_image_id")] + imageUuidList=[image_id] )] activation_params = dict( - schedule_validate=activation_details.get("scehduleValidate"), + schedule_validate=activation_details.get("scehdule_validate"), payload=payload ) log("Activation Params: " + str(activation_params)) @@ -858,24 +1132,32 @@ def get_diff_activation(self): ) task_details = {} task_id = response.get("response").get("taskId") + while (True): task_details = self.get_task_details(task_id) if not task_details.get("isError") and \ ("completed successfully" in task_details.get("progress")): self.result['changed'] = True - self.result['msg'] = "Image activated successfully" + self.result['msg'] = "Image Activated successfully" + self.status = "success" break if task_details.get("isError"): - self.module.fail_json(msg="Image Activation Failed", - response=task_details) + error_msg = "Activation for Image with Id - {0} gets Failed".format(image_id) + self.status = "failed" + self.result['response'] = task_details + self.msg = error_msg + return self self.result['response'] = task_details if task_details else response + return self + def get_diff_merged(self, config): """ Get tagging details and then trigger distribution followed by activation if specified in the playbook. Parameters: + self (object): An instance of a class used for interacting with Cisco DNA Center. config (dict): The configuration dictionary containing tagging, distribution, and activation details. Returns: self: The current instance of the class with updated 'result' and 'have' attributes. @@ -886,14 +1168,14 @@ def get_diff_merged(self, config): operations are successful, 'changed' is set to True. """ - if config.get("taggingDetails"): - self.get_diff_tagging() + if config.get("tagging_details"): + self.get_diff_tagging().check_return_status() - if config.get("imageDistributionDetails"): - self.get_diff_distribution() + if config.get("image_distribution_details"): + self.get_diff_distribution().check_return_status() - if config.get("imageActivationDetails"): - self.get_diff_activation() + if config.get("image_activation_details"): + self.get_diff_activation().check_return_status() return self From 89629b4a33c80a16a3d2a84ae9f448eeb7f54d85 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Wed, 6 Dec 2023 06:29:54 +0000 Subject: [PATCH 19/50] Adding discovery test module --- .../modules/dnac/test_discovery_intent.py | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 tests/unit/modules/dnac/test_discovery_intent.py diff --git a/tests/unit/modules/dnac/test_discovery_intent.py b/tests/unit/modules/dnac/test_discovery_intent.py new file mode 100644 index 0000000000..82f9999de0 --- /dev/null +++ b/tests/unit/modules/dnac/test_discovery_intent.py @@ -0,0 +1,170 @@ +# Copyright (c) 2020 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Make coding more python3-ish +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from ansible_collections.cisco.dnac.plugins.modules import discovery_intent +from .dnac_module import TestDnacModule, set_module_args + + +class TestDnacDiscoveryIntent(TestDnacModule): + def __init__(self): + + """ + Inheriting from the base class of dnac_module + """ + + module = discovery_intent + super().__init__(module) + + def load_fixtures(self, response=None, device=""): + + """ + Load fixtures for a specific device. + + Parameters: + response (list, optional): The expected response data. Defaults to None. + device (str, optional): The device for which to load fixtures. Defaults to an empty string. + """ + + if "create_discovery" in self._testMethodName: + self.run_dnac_exec.side_effect = [ + Exception(), + self.test_data.get("create_discovery_response"), + self.test_data.get("get_business_api_execution_details_response"), + self.test_data.get("get_discovery_response") + ] + elif "delete_existing_discovery" in self._testMethodName: + self.run_dnac_exec.side_effect = [ + self.test_data.get("delete_get_discovery_response"), + self.test_data.get("delete_delete_discovery_response"), + self.test_data.get("get_business_api_execution_details_response") + ] + elif "delete_non_existing_discovery" in self._testMethodName: + self.run_dnac_exec.side_effect = [ + Exception() + ] + elif "error_delete" in self._testMethodName: + self.run_dnac_exec.side_effect = [ + self.test_data.get("delete_error_get_discovery_response"), + self.test_data.get("delete_delete_discovery_response"), + self.test_data.get("delete_execution_details_error") + ] + elif "error_create" in self._testMethodName: + self.run_dnac_exec.side_effect = [ + Exception(), + self.test_data.get("create_discovery_response"), + self.test_data.get("delete_execution_details_error") + ] + + def test_discovery_intent_create_discovery(self): + set_module_args( + dict( + dnac_host="172.23.241.186", + dnac_username="admin", + dnac_password="Maglev123", + dnac_verify=False, + dnac_log=True, + state="merged", + headers=None, + name=self.playbook_config.get('name'), + devices_list=self.playbook_config.get('devices_list'), + discoveryType="MULTI RANGE", + protocolOrder="ssh", + startIndex=1, + recordsToReturn=25 + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertEqual( + result.get('msg'), + "Discovery Created Successfully" + ) + + def test_discovery_intent_delete_existing_discovery(self): + set_module_args( + dict( + dnac_host="172.23.241.186", + dnac_username="admin", + dnac_password="Maglev123", + dnac_verify=False, + dnac_log=True, + state="deleted", + headers=None, + name=self.playbook_config.get('name'), + devices_list=self.playbook_config.get('devices_list'), + discoveryType="MULTI RANGE", + protocolOrder="ssh", + startIndex=1, + recordsToReturn=25 + ) + ) + result = self.execute_module(changed=True, failed=False) + self.assertEqual( + result.get('msg'), + "Discovery Deleted Successfully" + ) + + def test_discovery_intent_delete_non_existing_discovery(self): + set_module_args( + dict( + dnac_host="172.23.241.186", + dnac_username="admin", + dnac_password="Maglev123", + dnac_verify=False, + dnac_log=True, + state="deleted", + headers=None, + name=self.playbook_config.get('delete_non_exist_discovery_name'), + devices_list=self.playbook_config.get('devices_list'), + discoveryType="MULTI RANGE", + protocolOrder="ssh", + startIndex=1, + recordsToReturn=25 + ) + ) + result = self.execute_module(changed=False, failed=False) + self.assertIsNone(result.get('exist_discovery')) + self.assertEqual( + result.get('msg'), + f"Discovery {self.playbook_config.get('delete_non_exist_discovery_name')} Not Found" + ) + + def test_discovery_intent_invalid_state(self): + + set_module_args( + dict( + dnac_host="172.23.241.186", + dnac_username="admin", + dnac_password="Maglev123", + dnac_verify=False, + dnac_log=True, + state="present", + headers=None, + name=self.playbook_config.get('name'), + devices_list=self.playbook_config.get('devices_list'), + discoveryType="MULTI RANGE", + protocolOrder="ssh", + startIndex=1, + recordsToReturn=25 + ) + ) + result = self.execute_module(changed=False, failed=True) + self.assertEqual( + result.get('msg'), + "value of state must be one of: merged, deleted, got: present" + ) From ea083380186d88656f24bf512bb8c1c7444aa03e Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 6 Dec 2023 21:24:06 +0530 Subject: [PATCH 20/50] remove redundant code --- plugins/modules/swim_intent.py | 426 +++++++++++++++++---------------- 1 file changed, 219 insertions(+), 207 deletions(-) diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index fed6633c8b..3d2234e5e7 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -536,29 +536,30 @@ def get_device_uuids(self, site_name, device_family, device_role): """ device_uuid_list = [] - if site_name: - site_exists = False - (site_exists, site_id) = self.site_exists(site_name) - if site_exists: - site_params = { - "site_id": site_id, - "device_family": device_family - } - response = self.dnac._exec( - family="sites", - function='get_membership', - op_modifies=True, - params=site_params, - ) - response = response['device'][0]['response'] - if len(response) > 0: - for item in response: - if item["reachabilityStatus"] != "Reachable": - continue - if "role" in item and device_role is not None and item["role"] == device_role.upper(): - device_uuid_list.append(item["instanceUuid"]) - elif device_role is None or device_role.upper() == "ALL": - device_uuid_list.append(item["instanceUuid"]) + if not site_name: + return device_uuid_list + + (site_exists, site_id) = self.site_exists(site_name) + if not site_exists: + return device_uuid_list + + site_params = { + "site_id": site_id, + "device_family": device_family + } + response = self.dnac._exec( + family="sites", + function='get_membership', + op_modifies=True, + params=site_params, + ) + response = response['device'][0]['response'] + if len(response) > 0: + for item in response: + if item["reachabilityStatus"] != "Reachable": + continue + if "role" in item and (device_role is None or item["role"] == device_role.upper() or device_role.upper() == "ALL"): + device_uuid_list.append(item["instanceUuid"]) return device_uuid_list @@ -755,65 +756,52 @@ def get_diff_import(self): """ try: - if self.want.get("import_type") == "url": + import_type = self.want.get("import_type") + if import_type == "url": image_name = self.want.get("url_import_details").get("payload")[0].get("source_url") + else: + image_name = self.want.get("local_import_details").get("filePath") - # Code to check if image already exist in the DNAC - name = image_name.split('/')[-1] - image_exist = self.is_image_exist(name) - if image_exist: - image_id = self.get_image_id(name) - self.have["imported_image_id"] = image_id - log_msg = "Image {0} already exists in the Cisco DNA Center".format(name) - self.result['msg'] = log_msg - log(log_msg) - self.status = "success" - self.result['changed'] = False - return self + # Code to check if the image already exists in DNAC + name = image_name.split('/')[-1] + image_exist = self.is_image_exist(name) + + if image_exist: + image_id = self.get_image_id(name) + self.have["imported_image_id"] = image_id + log_msg = "Image {0} already exists in the Cisco DNA Center".format(name) + self.result['msg'] = log_msg + self.log(log_msg) + self.status = "success" + self.result['changed'] = False + return self - url_import_params = dict( + if self.want.get("import_type") == "url": + import_params = dict( payload=self.want.get("url_import_details").get("payload"), schedule_at=self.want.get("url_import_details").get("schedule_at"), schedule_desc=self.want.get("url_import_details").get("schedule_desc"), schedule_origin=self.want.get("url_import_details").get("schedule_origin"), ) - response = self.dnac._exec( - family="software_image_management_swim", - function='import_software_image_via_url', - op_modifies=True, - params=url_import_params, - ) + import_function = 'import_software_image_via_url' else: - image_name = self.want.get("local_import_details").get("filePath") - # Code to check if image already exist in the DNAC - name = image_name.split('/')[-1] - image_exist = self.is_image_exist(name) - if image_exist: - image_id = self.get_image_id(name) - self.have["imported_image_id"] = image_id - log_msg = "Image {0} already exists in the Cisco DNA Center".format(name) - self.result['msg'] = log_msg - log(log_msg) - self.status = "success" - self.result['changed'] = False - return self - - local_import_params = dict( + import_params = dict( is_third_party=self.want.get("local_import_details").get("is_third_party"), third_party_vendor=self.want.get("local_import_details").get("third_party_vendor"), third_party_image_family=self.want.get("local_import_details").get("third_party_image_family"), third_party_application_type=self.want.get("local_import_details").get("third_party_application_type"), file_path=self.want.get("local_import_details").get("file_path"), ) - response = self.dnac._exec( - family="software_image_management_swim", - function='import_local_software_image', - op_modifies=True, - params=local_import_params, - file_paths=[('file_path', 'file')], - ) + import_function = 'import_local_software_image' - log(str(response)) + response = self.dnac._exec( + family="software_image_management_swim", + function=import_function, + op_modifies=True, + params=import_params, + ) + + self.log(str(response)) task_details = {} task_id = response.get("response").get("taskId") @@ -826,14 +814,14 @@ def get_diff_import(self): self.status = "success" log_msg = "Swim Image {0} imported successfully".format(name) self.result['msg'] = log_msg - log(log_msg) + self.log(log_msg) break if task_details and task_details.get("isError"): if "already exists" in task_details.get("failureReason"): log_msg = "SWIM Image {0} already exists in the Cisco DNA Center".format(name) self.result['msg'] = log_msg - log(log_msg) + self.log(log_msg) self.status = "success" self.result['changed'] = False break @@ -854,7 +842,6 @@ def get_diff_import(self): except Exception as e: self.log("Import Image details are not given in the playbook") - log("Import Image details are not given in the playbook without self") self.status = "success" self.result['changed'] = False @@ -945,55 +932,6 @@ def get_diff_distribution(self): device_uuid_list = self.get_device_uuids(site_name, device_family, device_role) image_id = self.have.get("distribution_image_id") - if len(device_uuid_list) > 0: - self.log("List of device UUID's " + str(device_uuid_list)) - device_distribution_count = 0 - for device_uuid in device_uuid_list: - distribution_params = dict( - payload=[dict( - deviceUuid=device_uuid, - imageUuid=image_id - )] - ) - log("Distribution Params: " + str(distribution_params)) - response = self.dnac._exec( - family="software_image_management_swim", - function='trigger_software_image_distribution', - op_modifies=True, - params=distribution_params, - ) - if response: - task_details = {} - task_id = response.get("response").get("taskId") - while (True): - task_details = self.get_task_details(task_id) - if not task_details.get("isError") and \ - ("completed successfully" in task_details.get("progress")): - self.result['changed'] = True - self.status = "success" - self.result['msg'] = "Image with Id {0} Distributed Successfully".format(image_id) - device_distribution_count += 1 - break - - if task_details.get("isError"): - error_msg = "Image with Id {0} Distribution Failed".format(image_id) - self.result['response'] = task_details - break - - if device_distribution_count == 0: - self.status = "failed" - error_msg = "Image with Id {0} Distribution Failed for all devices".format(image_id) - elif device_distribution_count == len(device_uuid_list): - self.result['changed'] = True - self.status = "success" - self.result['msg'] = "Image with Id {0} Distributed Successfully for all devices".format(image_id) - else: - self.result['changed'] = True - self.status = "success" - self.result['msg'] = "Image with Id {0} Distributed and partially Successfull".format(image_id) - - return self - if self.have.get("distribution_device_id"): distribution_params = dict( payload=[dict( @@ -1001,7 +939,7 @@ def get_diff_distribution(self): imageUuid=image_id )] ) - log("Distribution Params: " + str(distribution_params)) + self.log("Distribution Params: " + str(distribution_params)) response = self.dnac._exec( family="software_image_management_swim", @@ -1012,8 +950,10 @@ def get_diff_distribution(self): if response: task_details = {} task_id = response.get("response").get("taskId") + while (True): task_details = self.get_task_details(task_id) + if not task_details.get("isError") and \ ("completed successfully" in task_details.get("progress")): self.result['changed'] = True @@ -1030,6 +970,66 @@ def get_diff_distribution(self): self.result['response'] = task_details if task_details else response + return self + + if len(device_uuid_list) == 0: + self.status = "failed" + msg = "No devices found for Image Distribution" + self.result['msg'] = msg + self.log(msg) + return self + + self.log("List of device UUID's for Image Distribution " + str(device_uuid_list)) + device_distribution_count = 0 + for device_uuid in device_uuid_list: + distribution_params = dict( + payload=[dict( + deviceUuid=device_uuid, + imageUuid=image_id + )] + ) + log("Distribution Params: " + str(distribution_params)) + response = self.dnac._exec( + family="software_image_management_swim", + function='trigger_software_image_distribution', + op_modifies=True, + params=distribution_params, + ) + if response: + task_details = {} + task_id = response.get("response").get("taskId") + + while (True): + task_details = self.get_task_details(task_id) + + if not task_details.get("isError") and \ + ("completed successfully" in task_details.get("progress")): + self.result['changed'] = True + self.status = "success" + self.result['msg'] = "Image with Id {0} Distributed Successfully".format(image_id) + device_distribution_count += 1 + break + + if task_details.get("isError"): + error_msg = "Image with Id {0} Distribution Failed".format(image_id) + self.result['response'] = task_details + break + + if device_distribution_count == 0: + self.status = "failed" + msg = "Image with Id {0} Distribution Failed for all devices".format(image_id) + elif device_distribution_count == len(device_uuid_list): + self.result['changed'] = True + self.status = "success" + msg = "Image with Id {0} Distributed Successfully for all devices".format(image_id) + else: + self.result['changed'] = True + self.status = "success" + msg = "Image with Id {0} Distributed and partially Successfull".format(image_id) + + self.result['msg'] = msg + self.log(msg) + return self def get_diff_activation(self): @@ -1052,104 +1052,116 @@ def get_diff_activation(self): device_uuid_list = self.get_device_uuids(site_name, device_family, device_role) image_id = self.have.get("activation_image_id") - if len(device_uuid_list) > 0: - self.log("List of device UUID's " + str(device_uuid_list)) - device_activation_count = 0 - for device_uuid in device_uuid_list: - payload = [dict( - activateLowerImageVersion=activation_details.get("activate_lower_image_version"), - deviceUpgradeMode=activation_details.get("device_upgrade_mode"), - distributeIfNeeded=activation_details.get("distribute_if_needed"), - deviceUuid=device_uuid, - imageUuidList=[image_id] - )] + if self.have.get("activation_device_id"): + payload = [dict( + activateLowerImageVersion=activation_details.get("activate_lower_image_version"), + deviceUpgradeMode=activation_details.get("device_upgrade_mode"), + distributeIfNeeded=activation_details.get("distribute_if_needed"), + deviceUuid=self.have.get("activation_device_id"), + imageUuidList=[image_id] + )] + + activation_params = dict( + schedule_validate=activation_details.get("scehdule_validate"), + payload=payload + ) + self.log("Activation Params: " + str(activation_params)) - activation_params = dict( - schedule_validate=activation_details.get("scehdule_validate"), - payload=payload - ) - log("Activation Params: " + str(activation_params)) + response = self.dnac._exec( + family="software_image_management_swim", + function='trigger_software_image_activation', + op_modifies=True, + params=activation_params, + ) + task_details = {} + task_id = response.get("response").get("taskId") - response = self.dnac._exec( - family="software_image_management_swim", - function='trigger_software_image_activation', - op_modifies=True, - params=activation_params, - ) - if response: - task_details = {} - task_id = response.get("response").get("taskId") - while (True): - task_details = self.get_task_details(task_id) - if not task_details.get("isError") and \ - ("completed successfully" in task_details.get("progress")): - self.result['changed'] = True - self.status = "success" - self.result['msg'] = "Image with Id {0} Activated Successfully".format(image_id) - device_activation_count += 1 - break - - if task_details.get("isError"): - error_msg = "Image with Id {0} Activation Failed".format(image_id) - self.result['response'] = task_details - break - - if device_activation_count == 0: - self.status = "failed" - msg = "Image with Id {0} Activation Failed for all devices".format(image_id) - elif device_activation_count == len(device_uuid_list): - self.result['changed'] = True - self.status = "success" - msg = "Image with Id {0} Activated Successfully for all devices".format(image_id) - else: - self.result['changed'] = True - self.status = "success" - msg = "Image with Id {0} Activated and partially Successfull".format(image_id) + while (True): + task_details = self.get_task_details(task_id) + + if not task_details.get("isError") and \ + ("completed successfully" in task_details.get("progress")): + self.result['changed'] = True + self.result['msg'] = "Image Activated successfully" + self.status = "success" + break + + if task_details.get("isError"): + error_msg = "Activation for Image with Id - {0} gets Failed".format(image_id) + self.status = "failed" + self.result['response'] = task_details + self.msg = error_msg + return self + + self.result['response'] = task_details if task_details else response + + return self + + if len(device_uuid_list) == 0: + self.status = "failed" + msg = "No Devices found for Image Activation" self.result['msg'] = msg self.log(msg) - return self - payload = [dict( - activateLowerImageVersion=activation_details.get("activate_lower_image_version"), - deviceUpgradeMode=activation_details.get("device_upgrade_mode"), - distributeIfNeeded=activation_details.get("distribute_if_needed"), - deviceUuid=self.have.get("activation_device_id"), - imageUuidList=[image_id] - )] - - activation_params = dict( - schedule_validate=activation_details.get("scehdule_validate"), - payload=payload - ) - log("Activation Params: " + str(activation_params)) + # if len(device_uuid_list) > 0: + self.log("List of device UUID's for Image Activation" + str(device_uuid_list)) + device_activation_count = 0 + for device_uuid in device_uuid_list: + payload = [dict( + activateLowerImageVersion=activation_details.get("activate_lower_image_version"), + deviceUpgradeMode=activation_details.get("device_upgrade_mode"), + distributeIfNeeded=activation_details.get("distribute_if_needed"), + deviceUuid=device_uuid, + imageUuidList=[image_id] + )] + + activation_params = dict( + schedule_validate=activation_details.get("scehdule_validate"), + payload=payload + ) + log("Activation Params: " + str(activation_params)) - response = self.dnac._exec( - family="software_image_management_swim", - function='trigger_software_image_activation', - op_modifies=True, - params=activation_params, - ) - task_details = {} - task_id = response.get("response").get("taskId") + response = self.dnac._exec( + family="software_image_management_swim", + function='trigger_software_image_activation', + op_modifies=True, + params=activation_params, + ) + if response: + task_details = {} + task_id = response.get("response").get("taskId") - while (True): - task_details = self.get_task_details(task_id) - if not task_details.get("isError") and \ - ("completed successfully" in task_details.get("progress")): - self.result['changed'] = True - self.result['msg'] = "Image Activated successfully" - self.status = "success" - break + while (True): + task_details = self.get_task_details(task_id) - if task_details.get("isError"): - error_msg = "Activation for Image with Id - {0} gets Failed".format(image_id) - self.status = "failed" - self.result['response'] = task_details - self.msg = error_msg - return self + if not task_details.get("isError") and \ + ("completed successfully" in task_details.get("progress")): + self.result['changed'] = True + self.status = "success" + self.result['msg'] = "Image with Id {0} Activated Successfully".format(image_id) + device_activation_count += 1 + break + + if task_details.get("isError"): + error_msg = "Image with Id {0} Activation Failed".format(image_id) + self.result['response'] = task_details + break + + if device_activation_count == 0: + self.status = "failed" + msg = "Image with Id {0} Activation Failed for all devices".format(image_id) + elif device_activation_count == len(device_uuid_list): + self.result['changed'] = True + self.status = "success" + msg = "Image with Id {0} Activated Successfully for all devices".format(image_id) + else: + self.result['changed'] = True + self.status = "success" + msg = "Image with Id {0} Activated and partially Successfull".format(image_id) - self.result['response'] = task_details if task_details else response + self.result['msg'] = msg + self.log(msg) return self From 43b3f6bfbc31b16aca6b9b1bb00bcdc89066af7b Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Thu, 7 Dec 2023 21:50:51 +0530 Subject: [PATCH 21/50] Addressed the PR comments --- playbooks/template_pnp_intent.yml | 4 +- plugins/module_utils/dnac.py | 10 +- plugins/modules/device_credential_intent.py | 14 +- plugins/modules/network_settings_intent.py | 32 +- plugins/modules/template_intent.py | 546 +++++++++++++++++--- 5 files changed, 510 insertions(+), 96 deletions(-) diff --git a/playbooks/template_pnp_intent.yml b/playbooks/template_pnp_intent.yml index 61e870f114..fdeaf3356b 100644 --- a/playbooks/template_pnp_intent.yml +++ b/playbooks/template_pnp_intent.yml @@ -1,6 +1,6 @@ - hosts: dnac_servers vars_files: - - credentials.yml + - credentials_245.yml - device_details.yml gather_facts: false connection: local @@ -25,7 +25,7 @@ template_content: "{{ item.device_config }}" language: "{{ item.language }}" device_types: - - productFamily: "{{ item.family }}" + - product_family: "{{ item.family }}" software_type: "{{ item.type }}" software_variant: "{{ item.variant }}" template_name: "{{ item.temp_name }}" diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index a95db74e63..63a87dbc60 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -181,7 +181,13 @@ def check_task_response_status(self, response, validation_string, data=False): self.status = "exited" return self - task_id = response.get("response").get("taskId") + 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(str(task_details)) @@ -196,7 +202,7 @@ def check_task_response_status(self, response, validation_string, data=False): if validation_string in task_details.get("progress").lower(): self.result['changed'] = True - if data is True: + if data == True: self.msg = task_details.get("data") self.status = "success" break diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index a95be4b8ac..f4e64e7f26 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -2287,13 +2287,25 @@ def delete_device_credential(self, config): have_values = self.have.get("globalCredential") final_response = {} self.log(str(have_values)) + credential_mapping = { + "cliCredential": "cli_credential", + "snmpV2cRead": "snmp_v2c_read", + "snmpV2cWrite": "snmp_v2c_write", + "snmpV3": "snmp_v3", + "httpsRead": "https_read", + "httpsWrite": "https_write" + } for item in have_values: config_itr = 0 final_response.update({item: []}) for value in have_values.get(item): if value is None: + self.log(str(item)) + self.log(str(config.get("global_credential_details") + .get(credential_mapping.get(item)))) final_response.get(item).append( - str(config.get("global_credential_details").get(item)[config_itr]) + " is not found." + str(config.get("global_credential_details") + .get(credential_mapping.get(item))[config_itr]) + " is not found." ) continue _id = have_values.get(item)[config_itr].get("id") diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index 4286ccb72d..5440df80de 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -816,7 +816,9 @@ def get_network_params(self, site_id): "bannerMessage": messageoftheday_details.get("value")[0].get("bannerMessage"), } }) - if messageoftheday_details.get("value")[0].get("retainExistingBanner") is True: + retain_existing_banner = messageoftheday_details.get("value")[0] \ + .get("retainExistingBanner") + if retain_existing_banner is True: network_settings.get("messageOfTheday").update({ "retainExistingBanner": "true" }) @@ -826,11 +828,13 @@ def get_network_params(self, site_id): }) if network_aaa and network_aaa_pan: - if network_aaa_pan.get("value")[0] == "None": + aaa_pan_value = network_aaa_pan.get("value")[0] + aaa_value = network_aaa.get("value")[0] + if aaa_pan_value == "None": network_settings.update({ "network_aaa": { - "network": network_aaa.get("value")[0].get("ipAddress"), - "protocol": network_aaa.get("value")[0].get("protocol"), + "network": aaa_value.get("ipAddress"), + "protocol": aaa_value.get("protocol"), "ipAddress": network_aaa2.get("value")[0].get("ipAddress"), "servers": "AAA" } @@ -838,19 +842,21 @@ def get_network_params(self, site_id): else: network_settings.update({ "network_aaa": { - "network": network_aaa.get("value")[0].get("ipAddress"), - "protocol": network_aaa.get("value")[0].get("protocol"), - "ipAddress": network_aaa_pan.get("value")[0], + "network": aaa_value.get("ipAddress"), + "protocol": aaa_value.get("protocol"), + "ipAddress": aaa_pan_value, "servers": "ISE" } }) if clientAndEndpoint_aaa and clientAndEndpoint_aaa_pan: - if clientAndEndpoint_aaa_pan.get("value")[0] == "None": + aaa_pan_value = clientAndEndpoint_aaa_pan.get("value")[0] + aaa_value = clientAndEndpoint_aaa.get("value")[0] + if aaa_pan_value == "None": network_settings.update({ "clientAndEndpoint_aaa": { - "network": clientAndEndpoint_aaa.get("value")[0].get("ipAddress"), - "protocol": clientAndEndpoint_aaa.get("value")[0].get("protocol"), + "network": aaa_value.get("ipAddress"), + "protocol": aaa_value.get("protocol"), "ipAddress": clientAndEndpoint_aaa2.get("value")[0].get("ipAddress"), "servers": "AAA" } @@ -858,9 +864,9 @@ def get_network_params(self, site_id): else: network_settings.update({ "clientAndEndpoint_aaa": { - "network": clientAndEndpoint_aaa.get("value")[0].get("ipAddress"), - "protocol": clientAndEndpoint_aaa.get("value")[0].get("protocol"), - "ipAddress": clientAndEndpoint_aaa_pan.get("value")[0], + "network": aaa_value.get("ipAddress"), + "protocol": aaa_value.get("protocol"), + "ipAddress": aaa_pan_value, "servers": "ISE" } }) diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index 79ed72052f..ca0f661b55 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -65,13 +65,13 @@ type: list elements: dict suboptions: - productFamily: + product_family: description: Device family. type: str - productSeries: + product_series: description: Device series. type: str - productType: + product_type: description: Device type. type: str id: @@ -89,10 +89,10 @@ project_name: description: Name of the project under which templates are managed. type: str - projectDescription: + project_description: description: Description of the project created. type: str - rollbackTemplateParams: + rollback_template_params: description: Params required for template rollback. type: list elements: dict @@ -100,19 +100,19 @@ binding: description: Bind to source. type: str - customOrder: + custom_order: description: CustomOrder of template param. type: int - dataType: + data_type: description: Datatype of template param. type: str - defaultValue: + default_value: description: Default value of template param. type: str description: description: Description of template param. type: str - displayName: + display_name: description: Display name of param. type: str group: @@ -121,22 +121,22 @@ id: description: UUID of template param. type: str - instructionText: + instruction_text: description: Instruction text for param. type: str key: description: Key. type: str - notParam: + not_param: description: Is it not a variable. type: bool order: description: Order of template param. type: int - paramArray: + param_array: description: Is it an array. type: bool - parameterName: + parameter_name: description: Name of template param. type: str provider: @@ -150,10 +150,10 @@ id: description: UUID of range. type: str - maxValue: + max_value: description: Max value of range. type: int - minValue: + min_value: description: Min value of range. type: int required: @@ -162,17 +162,17 @@ selection: description: Configuration Template Create's selection. suboptions: - defaultSelectedValues: + default_selected_values: description: Default selection values. elements: str type: list id: description: UUID of selection. type: str - selectionType: + selection_type: description: Type of selection(SINGLE_SELECT or MULTI_SELECT). type: str - selectionValues: + selection_values: description: Selection values. type: dict type: dict @@ -190,26 +190,26 @@ template_content: description: Template content. type: str - templateParams: + template_params: description: Configuration Template Create's templateParams. elements: dict suboptions: binding: description: Bind to source. type: str - customOrder: + custom_order: description: CustomOrder of template param. type: int - dataType: + data_type: description: Datatype of template param. type: str - defaultValue: + default_value: description: Default value of template param. type: str description: description: Description of template param. type: str - displayName: + display_name: description: Display name of param. type: str group: @@ -218,22 +218,22 @@ id: description: UUID of template param. type: str - instructionText: + instruction_text: description: Instruction text for param. type: str key: description: Key. type: str - notParam: + not_param: description: Is it not a variable. type: bool order: description: Order of template param. type: int - paramArray: + param_array: description: Is it an array. type: bool - parameterName: + parameter_name: description: Name of template param. type: str provider: @@ -245,10 +245,10 @@ id: description: UUID of range. type: str - maxValue: + max_value: description: Max value of range. type: int - minValue: + min_value: description: Min value of range. type: int type: list @@ -259,17 +259,17 @@ selection: description: Configuration Template Create's selection. suboptions: - defaultSelectedValues: + default_selected_values: description: Default selection values. elements: str type: list id: description: UUID of selection. type: str - selectionType: + selection_type: description: Type of selection(SINGLE_SELECT or MULTI_SELECT). type: str - selectionValues: + selection_values: description: Selection values. type: dict type: dict @@ -291,13 +291,13 @@ device_types: description: Configuration Template Create's deviceTypes. This field is mandatory to create a new template. suboptions: - productFamily: + product_family: description: Device family. type: str - productSeries: + product_series: description: Device series. type: str - productType: + product_type: description: Device type. type: str type: list @@ -305,6 +305,9 @@ failure_policy: description: Define failure policy if template provisioning fails. type: str + id: + description: UUID of template. + type: str language: description: Template language choices: @@ -329,7 +332,7 @@ project_name: description: Project name. type: str - projectDescription: + project_description: description: Project Description. type: str rollback_template_content: @@ -341,19 +344,19 @@ binding: description: Bind to source. type: str - customOrder: + custom_order: description: CustomOrder of template param. type: int - dataType: + data_type: description: Datatype of template param. type: str - defaultValue: + default_value: description: Default value of template param. type: str description: description: Description of template param. type: str - displayName: + display_name: description: Display name of param. type: str group: @@ -362,22 +365,22 @@ id: description: UUID of template param. type: str - instructionText: + instruction_text: description: Instruction text for param. type: str key: description: Key. type: str - notParam: + not_param: description: Is it not a variable. type: bool order: description: Order of template param. type: int - paramArray: + param_array: description: Is it an array. type: bool - parameterName: + parameter_name: description: Name of template param. type: str provider: @@ -389,10 +392,10 @@ id: description: UUID of range. type: str - maxValue: + max_value: description: Max value of range. type: int - minValue: + min_value: description: Min value of range. type: int type: list @@ -403,17 +406,17 @@ selection: description: Configuration Template Create's selection. suboptions: - defaultSelectedValues: + default_selected_values: description: Default selection values. elements: str type: list id: description: UUID of selection. type: str - selectionType: + selection_type: description: Type of selection(SINGLE_SELECT or MULTI_SELECT). type: str - selectionValues: + selection_values: description: Selection values. type: dict type: dict @@ -448,19 +451,19 @@ binding: description: Bind to source. type: str - customOrder: + custom_order: description: CustomOrder of template param. type: int - dataType: + data_type: description: Datatype of template param. type: str - defaultValue: + default_value: description: Default value of template param. type: str description: description: Description of template param. type: str - displayName: + display_name: description: Display name of param. type: str group: @@ -469,22 +472,22 @@ id: description: UUID of template param. type: str - instructionText: + instruction_text: description: Instruction text for param. type: str key: description: Key. type: str - notParam: + not_param: description: Is it not a variable. type: bool order: description: Order of template param. type: int - paramArray: + param_array: description: Is it an array. type: bool - parameterName: + parameter_name: description: Name of template param. type: str provider: @@ -496,10 +499,10 @@ id: description: UUID of range. type: str - maxValue: + max_value: description: Max value of range. type: int - minValue: + min_value: description: Min value of range. type: int type: list @@ -510,17 +513,17 @@ selection: description: Configuration Template Create's selection. suboptions: - defaultSelectedValues: + default_selected_values: description: Default selection values. elements: str type: list id: description: UUID of selection. type: str - selectionType: + selection_type: description: Type of selection(SINGLE_SELECT or MULTI_SELECT). type: str - selectionValues: + selection_values: description: Selection values. type: dict type: dict @@ -529,18 +532,18 @@ validation_errors: description: Configuration Template Create's validationErrors. suboptions: - rollbackTemplateErrors: + rollback_template_errors: description: Validation or design conflicts errors of rollback template. elements: dict type: list - templateErrors: + template_errors: description: Validation or design conflicts errors. elements: dict type: list - templateId: + template_id: description: UUID of template. type: str - templateVersion: + template_version: description: Current version of template. type: str type: dict @@ -1285,6 +1288,7 @@ """ +import copy from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( DnacBase, @@ -1415,7 +1419,7 @@ def validate_input(self): 'parentTemplateId': {'type': 'str'}, 'projectId': {'type': 'str'}, 'project_name': {'type': 'str'}, - 'projectDescription': {'type': 'str'}, + 'project_description': {'type': 'str'}, 'rollbackTemplateContent': {'type': 'str'}, 'rollbackTemplateParams': {'type': 'list'}, 'software_type': {'type': 'str'}, @@ -1463,6 +1467,367 @@ def get_project_params(self, params): } return project_params + def get_tags(self, _tags): + """ + Store tags from the playbook for template processing in DNAC. + Check using check_return_status() + + Parameters: + tags (dict) - Tags details containing Template information. + + Returns: + tags (dict) - Organized tags parameters. + """ + + if _tags is None: + return None + + tags = [] + i = 0 + for item in _tags: + tags.append({}) + id = item.get("id") + if id is not None: + tags[i].update({"id": id}) + + name = item.get("name") + if name is not None: + tags[i].update({"name": name}) + else: + self.msg = "name is mandatory in tags in location " + str(i) + self.status = "failed" + return self.check_return_status() + + return tags + + def get_device_types(self, device_types): + """ + Store device types parameters from the playbook for template processing in DNAC. + Check using check_return_status() + + Parameters: + device_types (dict) - Device types details containing Template information. + + Returns: + deviceTypes (dict) - Organized device types parameters. + """ + + if device_types is None: + return None + + deviceTypes = [] + i = 0 + for item in device_types: + deviceTypes.append({}) + product_family = item.get("product_family") + if product_family is not None: + deviceTypes[i].update({"productFamily": product_family}) + else: + self.msg = "product_family is mandatory for deviceTypes" + 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}) + product_type = item.get("product_type") + if product_type is not None: + deviceTypes[i].update({"productType": product_type}) + i = i + 1 + + return deviceTypes + + def get_validation_errors(self, validation_errors): + """ + Store template parameters from the playbook for template processing in DNAC. + + Parameters: + validation_errors (dict) - Playbook details containing validation errors information. + + Returns: + validationErrors (dict) - Organized validation errors parameters. + """ + + if validation_errors is None: + return None + + validationErrors = {} + rollback_template_errors = validation_errors.get("rollback_template_errors") + if rollback_template_errors is not None: + validationErrors.update({ + "rollbackTemplateErrors": rollback_template_errors + }) + + template_errors = validation_errors.get("template_errors") + if template_errors is not None: + validationErrors.update({ + "templateErrors": template_errors + }) + + template_id = validation_errors.get("template_id") + if template_id is not None: + validationErrors.update({ + "templateId": template_id + }) + + template_version = validation_errors.get("template_version") + if template_version is not None: + validationErrors.update({ + "templateVersion": template_version + }) + + return validationErrors + + def get_template_info(self, template_params): + """ + Store template params from the playbook for template processing in DNAC. + Check using check_return_status() + + Parameters: + template_params (dict) - Playbook details containing template params information. + + Returns: + templateParams (dict) - Organized template params parameters. + """ + + if template_params is None: + return None + + templateParams = [] + i = 0 + for item in template_params: + templateParams.append({}) + binding = item.get("binding") + if binding is not None: + templateParams[i].update({"binding": binding}) + + custom_order = item.get("custom_order") + if custom_order is not None: + templateParams[i].update({"customOrder": custom_order}) + + default_value = item.get("default_value") + if default_value is not None: + templateParams[i].update({"defaultValue": default_value}) + + description = item.get("description") + if description is not None: + templateParams[i].update({"description": description}) + + display_name = item.get("display_name") + if display_name is not None: + templateParams[i].update({"displayName": display_name}) + + group = item.get("group") + if group is not None: + templateParams[i].update({"group": group}) + + id = item.get("id") + if id is not None: + templateParams[i].update({"id": id}) + + instruction_text = item.get("instruction_text") + if instruction_text is not None: + templateParams[i].update({"instructionText": instruction_text}) + + key = item.get("key") + if key is not None: + templateParams[i].update({"key": key}) + + not_param = item.get("not_param") + if not_param is not None: + templateParams[i].update({"notParam": not_param}) + + order = item.get("order") + if order is not None: + templateParams[i].update({"order": order}) + + param_array = item.get("param_array") + if param_array is not None: + templateParams[i].update({"paramArray": param_array}) + + provider = item.get("provider") + if provider is not None: + templateParams[i].update({"provider": provider}) + + parameter_name = item.get("parameter_name") + if parameter_name is not None: + templateParams[i].update({"parameterName": parameter_name}) + else: + self.msg = "parameter_name is mandatory for the template_params." + self.status = "failed" + return self.check_return_status() + + data_type = item.get("data_type") + datatypes = ["STRING", "INTEGER", "IPADDRESS", "MACADDRESS", "SECTIONDIVIDER"] + if data_type is not None: + templateParams[i].update({"dataType": data_type}) + else: + self.msg = "dataType is mandatory for the template_params." + self.status = "failed" + return self.check_return_status() + if data_type not in datatypes: + self.msg = "data_type under template_params should be in " + str(datatypes) + self.status = "failed" + return self.check_return_status() + + required = item.get("required") + if required is not None: + templateParams[i].update({"required": required}) + + range = item.get("range") + self.log(str(range)) + if range is not None: + templateParams[i].update({"range": []}) + _range = templateParams[i].get("range") + self.log(str(_range)) + j = 0 + for value in range: + _range.append({}) + id = range[j].get("id") + if id is not None: + _range.update({"id": id}) + max_value = range[j].get("max_value") + if max_value is not None: + _range.update({"maxValue": max_value}) + else: + self.msg = "max_value is mandatory for range under template_params" + self.status = "failed" + return self.check_return_status() + min_value = range[j].get("min_value") + if min_value is not None: + _range.update({"maxValue": min_value}) + else: + self.msg = "min_value is mandatory for range under template_params" + self.status = "failed" + return self.check_return_status() + j = j + 1 + + selection = item.get("selection") + if selection is not None: + templateParams[i].update({"selection": {}}) + _selection = templateParams[i].get("selection") + id = selection.get("id") + if id is not None: + _selection.update({"id": id}) + default_selected_values = selection.get("default_selected_values") + if default_selected_values is not None: + _selection.update({"defaultSelectedValues": default_selected_values}) + selection_type = selection.get("selection_type") + selectiontypes = ["SINGLE_SELECT", "MULTI_SELECT"] + if selection_type is not None: + _selection.update({"selectionType": selection_type}) + else: + self.msg = "selection_type is mandatory in selection." + self.status = "failed" + return self.check_return_status() + if selection_type not in selectiontypes: + self.msg = "selection_type under selection must be in " + str(selectiontypes) + self.status = "failed" + return self.check_return_status() + selection_values = selection.get("selection_values") + if selection_values is not None: + _selection.update({"selectionValues": selection_values}) + else: + self.msg = "selection_values is mandatory in selection" + self.status = "failed" + return self.check_return_status() + i = i + 1 + + return templateParams + + def get_containing_templates(self, containing_templates): + """ + Store tags from the playbook for template processing in DNAC. + Check using check_return_status() + + Parameters: + containing_templates (dict) - Containing tempaltes details + containing Template information. + + Returns: + containingTemplates (dict) - Organized containing templates parameters. + """ + + if containing_templates is None: + return None + + containingTemplates = [] + i = 0 + for item in containing_templates: + containingTemplates.append({}) + _tags = item.get("tags") + if _tags is not None: + containingTemplates[i].update({"tags": self.get_tags(_tags)}) + + composite = item.get("composite") + if composite is not None: + containingTemplates[i].update({"composite": composite}) + + description = item.get("description") + if description is not None: + containingTemplates[i].update({"description": description}) + + device_types = item.get("device_types") + if device_types is not None: + containingTemplates[i].update({ + "deviceTypes": self.get_device_types(device_types) + }) + + id = item.get("id") + if id is not None: + containingTemplates[i].update({"id": id}) + + language = item.get("language") + if language is not None: + containingTemplates[i].update({"language": language}) + else: + self.msg = "language is mandatory under containing templates" + self.status = "failed" + return self.check_return_status() + + name = item.get("name") + name_list = ["JINJA", "VELOCITY"] + if name is not None: + containingTemplates[i].update({"name": name}) + else: + self.msg = "name is mandatory under containing templates" + self.status = "failed" + return self.check_return_status() + if name not in name_list: + self.msg = "name under containing templates should be in " + str(name_list) + self.status = "failed" + return self.check_return_status() + + project_name = item.get("project_name") + if project_name is not None: + containingTemplates[i].update({"projectName": project_name}) + else: + self.msg = "project_name is mandatory under containing templates" + self.status = "failed" + return self.check_return_status() + + rollback_template_params = item.get("rollback_template_params") + if rollback_template_params is not None: + containingTemplates[i].update({ + "rollbackTemplateParams": self.get_template_info(rollback_template_params) + }) + + template_content = item.get("template_content") + if template_content is not None: + containingTemplates[i].update({"templateContent": template_content}) + + template_params = item.get("template_params") + if template_params is not None: + containingTemplates[i].update({ + "templateParams": self.get_template_info(template_params) + }) + + version = item.get("version") + if version is not None: + containingTemplates[i].update({"version": version}) + + return containingTemplates + def get_template_params(self, params): """ Store template parameters from the playbook for template processing in DNAC. @@ -1476,16 +1841,21 @@ def get_template_params(self, params): configuration_templates = params.get("configuration_templates") temp_params = { - "tags": configuration_templates.get("template_tag"), + "tags": self.get_tags(configuration_templates + .get("template_tag")), "author": configuration_templates.get("author"), "composite": configuration_templates.get("composite"), - "containingTemplates": configuration_templates.get("containing_templates"), + "containingTemplates": + self.get_containing_templates( + configuration_templates.get("containing_templates") + ), "createTime": configuration_templates.get("create_time"), "customParamsOrder": configuration_templates.get("custom_params_order"), "description": configuration_templates.get("template_description"), - "deviceTypes": configuration_templates.get("device_types"), + "deviceTypes": + self.get_device_types(configuration_templates.get("device_types")), "failurePolicy": configuration_templates.get("failure_policy"), - "id": configuration_templates.get("templateId"), + "id": configuration_templates.get("id"), "language": configuration_templates.get("language").upper(), "lastUpdateTime": configuration_templates.get("last_update_time"), "latestVersionTime": configuration_templates.get("latest_version_time"), @@ -1494,16 +1864,27 @@ def get_template_params(self, params): "projectId": configuration_templates.get("project_id"), "projectName": configuration_templates.get("project_name"), "rollbackTemplateContent": configuration_templates.get("rollback_template_content"), - "rollbackTemplateParams": configuration_templates.get("rollback_template_params"), + "rollbackTemplateParams": + self.get_template_info(configuration_templates.get("rollback_template_params")), "softwareType": configuration_templates.get("software_type"), "softwareVariant": configuration_templates.get("software_variant"), "softwareVersion": configuration_templates.get("software_version"), "templateContent": configuration_templates.get("template_content"), - "templateParams": configuration_templates.get("template_params"), - "validationErrors": configuration_templates.get("validation_errors"), + "templateParams": + self.get_template_info( + configuration_templates.get("template_params") + ), + "validationErrors": + self.get_validation_errors(configuration_templates.get("validation_errors")), "version": configuration_templates.get("version"), - "project_id": configuration_templates.get("project_id"), + "project_id": configuration_templates.get("project_id") } + self.log(str(temp_params)) + copy_temp_params = copy.deepcopy(temp_params) + for item in copy_temp_params: + if temp_params[item] is None: + del temp_params[item] + self.log(str(temp_params)) return temp_params def get_template(self, config): @@ -1802,6 +2183,8 @@ def requires_update(self): current_obj = self.have_template.get("template") requested_obj = self.want.get("template_params") + self.log(str(current_obj)) + self.log(str(requested_obj)) obj_params = [ ("tags", "tags", ""), ("author", "author", ""), @@ -1917,6 +2300,8 @@ def get_export_template_values(self, export_values): for values in export_values: self.log(str(values.get("projectName"))) template_details = template_details.get("response") + self.log(str(template_details)) + self.log(str(values.get("projectName"))) all_template_details = get_dict_result(template_details, "name", values.get("projectName")) @@ -1937,7 +2322,7 @@ def get_export_template_values(self, export_values): self.status = "success" return self - def export_project_or_tempalte(self, export): + def export_project_or_template(self, export): """ Export templates and projects in DNAC with fields provided in DNAC. @@ -2059,11 +2444,17 @@ def get_diff_merged(self, config): is_template_found = self.have_template.get("template_found") template_params = self.want.get("template_params") + self.log(str(template_params)) + self.log(str(self.have_template)) template_id = None template_updated = False self.validate_input_merge(is_template_found).check_return_status() if is_template_found: if self.requires_update(): + template_id = self.have_template.get("id") + template_params.update({"id": template_id}) + self.log(str(self.have_template)) + self.log(str(self.want)) response = self.dnac_apply['exec']( family="configuration_templates", function="update_template", @@ -2071,7 +2462,6 @@ def get_diff_merged(self, config): op_modifies=True, ) template_updated = True - template_id = self.have_template.get("id") self.log("Updating Existing Template") else: # Template does not need update @@ -2119,7 +2509,7 @@ def get_diff_merged(self, config): export = config.get("export") if export: - self.export_project_or_tempalte(export) + self.export_project_or_template(export) _import = config.get("import") if _import: From c4d2e014c9cb89bc80af4166620509677d6c18b0 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Thu, 7 Dec 2023 22:04:45 +0530 Subject: [PATCH 22/50] Addressed the PR comments --- plugins/module_utils/dnac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 63a87dbc60..b8a42828d2 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -202,7 +202,7 @@ def check_task_response_status(self, response, validation_string, data=False): if validation_string in task_details.get("progress").lower(): self.result['changed'] = True - if data == True: + if data is True: self.msg = task_details.get("data") self.status = "success" break From 8bc0454333a5bfb81570454d8336402b926a1266 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 8 Dec 2023 13:18:48 +0530 Subject: [PATCH 23/50] Delete entire site in one shot --- plugins/modules/site_intent.py | 66 ++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index a9845f9d21..3f19084c1e 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -307,11 +307,11 @@ ) floor_plan = { - '57057': 'CUBES AND WALLED OFFICES', - '57058': 'DRYWELL OFFICE ONLY', - '41541500': 'FREE SPACE', - '57060': 'INDOOR HIGH CEILING', - '57059': 'OUTDOOR OPEN SPACE' + '101101': 'Cubes And Walled Offices', + '101102': 'Drywall Office Only', + '101105': 'Free Space', + '101104': 'Indoor High Ceiling', + '101103': 'Outdoor Open Space' } @@ -403,7 +403,7 @@ def get_current_site(self, site): building=dict( name=site[0].get("name"), parentName=site[0].get("siteNameHierarchy").split("/" + site[0].get("name"))[0], - address=location.get("attributes").get("address", ""), + address=location.get("attributes").get("address"), latitude=location.get("attributes").get("latitude"), longitude=location.get("attributes").get("longitude"), ) @@ -509,7 +509,7 @@ def get_site_params(self, params): building_details = params.get('site').get('building') site_info['building'] = { 'name': building_details.get('name'), - 'address': building_details.get('address', ""), + 'address': building_details.get('address'), 'parentName': building_details.get('parent_name'), 'latitude': building_details.get('latitude'), 'longitude': building_details.get('longitude'), @@ -580,6 +580,15 @@ def site_requires_update(self): self.log("Current Site: " + str(current_site)) self.log("Requested Site: " + str(requested_site)) + if requested_site.get('type') == "building": + requested_address = requested_site.get('site').get('building').get('address') + current_address = current_site.get('site').get('building').get('address') + + if requested_address is None or requested_address == current_address: + return False + + return True + obj_params = [ ("type", "type"), ("site", "site") @@ -774,10 +783,47 @@ def get_diff_deleted(self, config): site_exists = self.have.get("site_exists") if site_exists: + # Check here if the site have the childs then fetch it using get membership API and then sort it + # in reverse order and start deleting from bottom to top + site_id = self.have.get("site_id") + mem_response = self.dnac._exec( + family="sites", + function="get_membership", + params={"site_id": site_id}, + ) + site_response = mem_response.get("site").get("response") + + if len(site_response) > 0: + # Sorting the response in reverse order based on hierarchy levels + sorted_site_resp = sorted(site_response, key=lambda x: x.get("groupHierarchy"), reverse=True) + # Deleting each level in reverse order till topmost parent site + for item in sorted_site_resp: + response = self.dnac._exec( + family="sites", + function="delete_site", + params={"site_id": item['id']}, + ) + + if response and isinstance(response, dict): + executionid = response.get("executionId") + while True: + execution_details = self.get_execution_details(executionid) + if execution_details.get("status") == "SUCCESS": + self.result['changed'] = True + self.log("Site - {0} deleted successfully".format(item['name'])) + break + + elif execution_details.get("bapiError"): + self.module.fail_json(msg=execution_details.get("bapiError"), + response=execution_details) + break + # print(f"Deleting {item['name']} ({item['groupTypeList'][0]}) with ID {item['id']}") + + # Delete the given site in the playbook response = self.dnac._exec( family="sites", function="delete_site", - params={"site_id": self.have.get("site_id")}, + params={"site_id": site_id}, ) if response and isinstance(response, dict): @@ -787,8 +833,8 @@ def get_diff_deleted(self, config): if execution_details.get("status") == "SUCCESS": self.result['changed'] = True self.result['response'] = execution_details - self.result['response'].update({"siteId": self.have.get("site_id")}) - self.result['msg'] = "Site - {0} deleted successfully".format(self.want.get("site_name")) + self.result['response'].update({"siteId": site_id}) + self.result['msg'] = "Site - {0} and it's child deleted successfully".format(self.want.get("site_name")) break elif execution_details.get("bapiError"): From e529be7e4379274a9cd561795954e23b97c607e0 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 8 Dec 2023 17:12:09 +0530 Subject: [PATCH 24/50] optimised code for site deletion --- plugins/modules/site_intent.py | 188 +++++++++++++++++---------------- 1 file changed, 95 insertions(+), 93 deletions(-) diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 3f19084c1e..6f4e16852b 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -373,7 +373,7 @@ def get_current_site(self, site): """ Get the current site information. Parameters: - - self (object): An instance of the class containing the method. + self (object): An instance of a class used for interacting with Cisco DNA Center. - site (list): A list containing information about the site. Returns: - dict: A dictionary containing the extracted site information. @@ -484,6 +484,7 @@ def get_site_params(self, params): Store the site-related parameters. Parameters: + self (object): An instance of a class used for interacting with Cisco DNA Center. - params (dict): Dictionary containing site-related parameters. Returns: - dict: Dictionary containing the stored site-related parameters. @@ -542,6 +543,7 @@ def get_site_name(self, site): Get and Return the site name. Parameters: + self (object): An instance of a class used for interacting with Cisco DNA Center. - site (dict): A dictionary containing information about the site. Returns: - str: The constructed site name. @@ -564,14 +566,14 @@ def site_requires_update(self): Check if the site requires updates. Parameters: - - site (dict): A dictionary containing information about the site. + self (object): An instance of a class used for interacting with Cisco DNA Center. Returns: - - bool: True if the site requires updates, False otherwise. + bool: True if the site requires updates, False otherwise. Description: This method compares the site parameters of the current site - ('current_site') and the requested site parameters ('requested_site') - stored in the 'want' attribute. It checks for differences in - specified parameters, such as the site type and site details. + ('current_site') and the requested site parameters ('requested_site') + stored in the 'want' attribute. It checks for differences in + specified parameters, such as the site type and site details. """ requested_site = self.want.get("site_params") @@ -581,8 +583,8 @@ def site_requires_update(self): self.log("Requested Site: " + str(requested_site)) if requested_site.get('type') == "building": - requested_address = requested_site.get('site').get('building').get('address') - current_address = current_site.get('site').get('building').get('address') + requested_address = requested_site['site']['building']['address'] + current_address = current_site['site']['building']['address'] if requested_address is None or requested_address == current_address: return False @@ -637,14 +639,15 @@ def get_want(self, config): creation in Cisco DNA Center. Parameters: - - config (dict): A dictionary containing configuration information. + self (object): An instance of a class used for interacting with Cisco DNA Center. + config (dict): A dictionary containing configuration information. Returns: - - object: The instance of the class. + self (object): An instance of a class used for interacting with Cisco DNA Center. Description: Retrieves all site-related information from playbook that is - required for creating a site in Cisco DNA Center. It includes - parameters such as 'site_params' and 'site_name.' The gathered - information is stored in the 'want' attribute for later reference. + required for creating a site in Cisco DNA Center. It includes + parameters such as 'site_params' and 'site_name.' The gathered + information is stored in the 'want' attribute for later reference. """ want = {} @@ -663,25 +666,17 @@ def get_diff_merged(self, config): provided in the playbook. Parameters: - - config (dict): A dictionary containing configuration information. + self (object): An instance of a class used for interacting with Cisco DNA Center. + config (dict): A dictionary containing configuration information. Returns: - - object: The instance of the class. The result dictionary includes - the following keys: - - 'changed' (bool): Indicates whether changes were made - during the update or creation process. - - 'response' (dict): Contains details about the execution - and the updated or created site ID. - - 'msg' (str): A message indicating the status of the - update or creation operation. + self (object): An instance of a class used for interacting with Cisco DNA Center. Description: - This method determines whether to update or create a site in - Cisco DNA Center based on the provided configuration information. If - the specified site exists, the method checks if it requires an update - by calling the 'site_requires_update' method. If an update is - required, it calls the 'update_site' function from the 'sites' family - of the Cisco DNA Center API. If the site does not require an update, - the method exits, indicating that the site is up to date. + This method determines whether to update or create a site in Cisco DNA Center based on the provided + configuration information. If the specified site exists, the method checks if it requires an update + by calling the 'site_requires_update' method. If an update is required, it calls the 'update_site' + function from the 'sites' family of the Cisco DNA Center API. If the site does not require an update, + the method exits, indicating that the site is up to date. """ site_updated = False @@ -755,6 +750,52 @@ def get_diff_merged(self, config): return self + def delete_single_site(self, site_id, site_name): + """" + Delete a single site in the Cisco DNA Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco DNA Center. + site_id (str): The ID of the site to be deleted. + site_name (str): The name of the site to be deleted. + Returns: + self (object): An instance of a class used for interacting with Cisco DNA Center. + Description: + This function initiates the deletion of a site in the Cisco DNA Center by calling the delete API. + If the deletion is successful, the result is marked as changed, and the status is set to "success." + If an error occurs during the deletion process, the status is set to "failed," and the log contains + details about the error. + """ + + try: + response = self.dnac._exec( + family="sites", + function="delete_site", + params={"site_id": site_id}, + ) + + if response and isinstance(response, dict): + executionid = response.get("executionId") + while True: + execution_details = self.get_execution_details(executionid) + if execution_details.get("status") == "SUCCESS": + msg = "Site - {0} deleted successfully".format(site_name) + self.result['changed'] = True + self.result['response'] = msg + self.status = "success" + self.log(msg) + break + elif execution_details.get("bapiError"): + self.module.fail_json(msg=execution_details.get("bapiError"), response=execution_details) + break + + except Exception as e: + msg = "Cannot Delete device from Inventory because of {0}".format(str(e)) + self.log(msg) + self.status = "failed" + self.msg = msg + + return self + def get_diff_deleted(self, config): """ Call Cisco DNA Center API to delete sites with provided inputs. @@ -781,74 +822,35 @@ def get_diff_deleted(self, config): """ site_exists = self.have.get("site_exists") + if not site_exists: + msg = msg = "Cannot delete Site - {0} as it's not found in Cisco DNA Center".format(self.want.get("site_name")) + self.result.update({'changed': False, + 'response': msg, + 'msg': msg}) + self.log(msg) + self.status = "success" + return self - if site_exists: - # Check here if the site have the childs then fetch it using get membership API and then sort it - # in reverse order and start deleting from bottom to top - site_id = self.have.get("site_id") - mem_response = self.dnac._exec( - family="sites", - function="get_membership", - params={"site_id": site_id}, - ) - site_response = mem_response.get("site").get("response") - - if len(site_response) > 0: - # Sorting the response in reverse order based on hierarchy levels - sorted_site_resp = sorted(site_response, key=lambda x: x.get("groupHierarchy"), reverse=True) - # Deleting each level in reverse order till topmost parent site - for item in sorted_site_resp: - response = self.dnac._exec( - family="sites", - function="delete_site", - params={"site_id": item['id']}, - ) - - if response and isinstance(response, dict): - executionid = response.get("executionId") - while True: - execution_details = self.get_execution_details(executionid) - if execution_details.get("status") == "SUCCESS": - self.result['changed'] = True - self.log("Site - {0} deleted successfully".format(item['name'])) - break - - elif execution_details.get("bapiError"): - self.module.fail_json(msg=execution_details.get("bapiError"), - response=execution_details) - break - # print(f"Deleting {item['name']} ({item['groupTypeList'][0]}) with ID {item['id']}") - - # Delete the given site in the playbook - response = self.dnac._exec( - family="sites", - function="delete_site", - params={"site_id": site_id}, - ) + # Check here if the site have the childs then fetch it using get membership API and then sort it + # in reverse order and start deleting from bottom to top + site_id = self.have.get("site_id") + mem_response = self.dnac._exec( + family="sites", + function="get_membership", + params={"site_id": site_id}, + ) + site_response = mem_response.get("site").get("response") - if response and isinstance(response, dict): - executionid = response.get("executionId") - while True: - execution_details = self.get_execution_details(executionid) - if execution_details.get("status") == "SUCCESS": - self.result['changed'] = True - self.result['response'] = execution_details - self.result['response'].update({"siteId": site_id}) - self.result['msg'] = "Site - {0} and it's child deleted successfully".format(self.want.get("site_name")) - break + if len(site_response) == 0: + self.delete_single_site(site_id, self.want.get("site_name")) + return self - elif execution_details.get("bapiError"): - self.module.fail_json(msg=execution_details.get("bapiError"), - response=execution_details) - break + # Sorting the response in reverse order based on hierarchy levels + sorted_site_resp = sorted(site_response, key=lambda x: x.get("groupHierarchy"), reverse=True) - else: - msg = "Cannot delete Site - {0} as it's not found in Cisco DNA Center".format(self.want.get("site_name")) - self.status = "success" - self.result['changed'] = False - self.result['response'] = msg - self.log(msg) - self.result['msg'] = msg + # Deleting each level in reverse order till topmost parent site + for item in sorted_site_resp: + self.delete_single_site(item['id'], item['name']) return self From 1ce8df30afc18a856677b3c69965405c76930459 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 8 Dec 2023 22:39:10 +0530 Subject: [PATCH 25/50] added code for deleting parent site after deleting all child sites --- plugins/modules/site_intent.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 6f4e16852b..83f412f8b2 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -852,6 +852,12 @@ def get_diff_deleted(self, config): for item in sorted_site_resp: self.delete_single_site(item['id'], item['name']) + # Delete the final parent site + self.delete_single_site(site_id, self.want.get("site_name")) + msg = "Site - {0} and it's child sites deleted successfully".format(self.want.get("site_name")) + self.result['response'] = msg + self.log(msg) + return self From d47bc0c25dcf43e204fa92f398559db852c2276b Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Sun, 10 Dec 2023 01:14:13 +0530 Subject: [PATCH 26/50] Addressed PR comments --- plugins/modules/device_credential_intent.py | 241 ++++++---- plugins/modules/template_intent.py | 492 ++++++++++---------- 2 files changed, 407 insertions(+), 326 deletions(-) diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index f4e64e7f26..9944119d5e 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -5,6 +5,7 @@ """Ansible module to perform operations on device credentials in Cisco DNA Center.""" from __future__ import absolute_import, division, print_function +import string __metaclass__ = type __author__ = ['Muthu Rakesh, Madhan Sankaranarayanan'] @@ -220,55 +221,79 @@ description: Assign Device Credentials to Site. type: dict suboptions: - cli_description: - description: CLI Credential Description. - type: str - cli_username: - description: CLI Credential Username. - type: str - cli_id: - description: CLI Credential Id. Use (Description, Username) or Id. - type: str - http_read_description: - description: HTTP(S) Read Credential Description. - type: str - http_read_username: - description: HTTP(S) Read Credential Username. - type: str - http_read: - description: HTTP(S) Read Credential Id. Use (Description, Username) or Id. - type: str - http_write_description: - description: HTTP(S) Write Credential Description. - type: str - http_write_username: - description: HTTP(S) Write Credential Username. - type: str - http_write: - description: HTTP(S) Write Credential Id. Use (Description, Username) or Id. - type: str + cli_credential: + description: CLI Credential. + type: dict + suboptions: + description: + description: CLI Credential Description. + type: str + username: + description: CLI Credential Username. + type: str + id: + description: CLI Credential Id. Use (Description, Username) or Id. + type: str + https_read: + description: HTTP(S) Read Credential + type: dict + suboptions: + description: + description: HTTP(S) Read Credential Description. + type: str + username: + description: HTTP(S) Read Credential Username. + type: str + id: + description: HTTP(S) Read Credential Id. Use (Description, Username) or Id. + type: str + https_write: + description: HTTP(S) Write Credential + type: dict + suboptions: + description: + description: HTTP(S) Write Credential Description. + type: str + username: + description: HTTP(S) Write Credential Username. + type: str + id: + description: HTTP(S) Write Credential Id. Use (Description, Username) or Id. + type: str site_name: description: Site Name to assign credential. type: list elements: str - snmp_v2_read_description: - description: SNMPv2c Read Credential Description. - type: str - snmp_v2_read_id: - description: SNMPv2c Read Credential Id. Use Description or Id. - type: str - snmp_v2_write_description: - description: SNMPv2c Write Credential Description. - type: str - snmp_v2_write_id: - description: SNMPv2c Write Credential Id. Use Description or Id. - type: str - snmp_v3_description: - description: snmp_v3 Credential Description. - type: str - snmp_v3_id: - description: snmp_v3 Credential Id. Use Description or Id. - type: str + snmp_v2c_read: + description: SNMPv2c Read Credential + type: dict + suboptions: + description: + description: SNMPv2c Read Credential Description. + type: str + id: + description: SNMPv2c Read Credential Id. Use Description or Id. + type: str + snmp_v2c_write: + description: SNMPv2c Write Credential + type: dict + suboptions: + description: + description: SNMPv2c Write Credential Description. + type: str + id: + description: SNMPv2c Write Credential Id. Use Description or Id. + type: str + snmp_v3: + description: snmp_v3 Credential + type: dict + suboptions: + description: + description: snmp_v3 Credential Description. + type: str + id: + description: snmp_v3 Credential Id. Use Description or Id. + type: str requirements: - dnacentersdk >= 2.5.5 - python >= 3.5 @@ -343,12 +368,18 @@ password: string port: 443 assign_credentials_to_site: - cli_id: string - snmp_v2_read_id: string - snmp_v2_write_id: string - snmp_v3_id: string - http_read: string - http_write: string + cli_credential: + id: string + snmp_v2c_read: + id: string + snmp_v2c_write: + id: string + snmp_v3: + id: string + https_read: + id: string + https_write: + id: string site_name: - string @@ -604,15 +635,21 @@ state: merged config: - assign_credentials_to_site: - cli_description: string - cli_username: string - snmp_v2_read_description: string - snmp_v2_write_description: string - snmp_v3_description: string - http_read_description: string - http_read_username: string - http_write_username: string - http_write_description: string + cli_credential: + description: string + username: string + snmp_v2c_read: + description: string + snmp_v2c_write: + description: string + snmp_v3: + description: string + https_read: + description: string + username: string + https_write: + description: string + username: string site_name: - string - string @@ -758,22 +795,44 @@ def validate_input(self): }, "assign_credentials_to_site": { "type": 'dict', - "cli_description": {"type": 'string'}, - "cli_username": {"type": 'string'}, - "cli_id": {"type": 'string'}, - "snmp_v2_read_description": {"type": 'string'}, - "snmp_v2_read_id": {"type": 'string'}, - "snmp_v2_write_description": {"type": 'string'}, - "snmp_v2_write_id": {"type": 'string'}, - "snmp_v3_description": {"type": 'string'}, - "snmp_v3_id": {"type": 'string'}, - "http_read_description": {"type": 'string'}, - "http_read_username": {"type": 'string'}, - "http_read": {"type": 'string'}, - "http_write_description": {"type": 'string'}, - "http_write_username": {"type": 'string'}, - "http_write": {"type": 'string'}, - "site_name": {"type": 'list'} + "cli_credential": { + "type": 'dict', + "description": {"type: 'string'"}, + "username": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "snmp_v2c_read": { + "type": 'dict', + "description": {"type: 'string'"}, + "username": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "snmp_v2c_write": { + "type": 'dict', + "description": {"type: 'string'"}, + "id": {"type": 'string'}, + }, + "snmp_v3": { + "type": 'dict', + "description": {"type: 'string'"}, + "id": {"type": 'string'}, + }, + "https_read": { + "type": 'dict', + "description": {"type: 'string'"}, + "username": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "https_write": { + "type": 'dict', + "description": {"type: 'string'"}, + "username": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "site_name": { + "type": 'list', + "elements": 'string' + } } } @@ -1885,9 +1944,9 @@ def get_want_assign_credentials(self, AssignCredentials): site_id.append(siteId) want.update({"site_id": site_id}) global_credentials = self.get_global_credentials_params() - cliId = AssignCredentials.get("cli_id") - cliDescription = AssignCredentials.get("cli_description") - cliUsername = AssignCredentials.get("cli_username") + cliId = AssignCredentials.get("cli_credential").get("id") + cliDescription = AssignCredentials.get("cli_credential").get("description") + cliUsername = AssignCredentials.get("cli_credential").get("username") if cliId or cliDescription and cliUsername: # All CLI details from the Cisco DNA Center @@ -1914,8 +1973,8 @@ def get_want_assign_credentials(self, AssignCredentials): return self want.get("assign_credentials").update({"cliId": cliDetail.get("id")}) - snmpV2cReadId = AssignCredentials.get("snmp_v2_read_id") - snmpV2cReadDescription = AssignCredentials.get("snmp_v2_read_description") + snmpV2cReadId = AssignCredentials.get("snmp_v2c_read").get("id") + snmpV2cReadDescription = AssignCredentials.get("snmp_v2c_read").get("description") if snmpV2cReadId or snmpV2cReadDescription: # All snmpV2cRead details from the Cisco DNA Center @@ -1941,8 +2000,8 @@ def get_want_assign_credentials(self, AssignCredentials): return self want.get("assign_credentials").update({"snmpV2ReadId": snmpV2cReadDetail.get("id")}) - snmpV2cWriteId = AssignCredentials.get("snmp_v2_write_id") - snmpV2cWriteDescription = AssignCredentials.get("snmp_v2_write_description") + snmpV2cWriteId = AssignCredentials.get("snmp_v2c_write").get("id") + snmpV2cWriteDescription = AssignCredentials.get("snmp_v2c_write").get("description") if snmpV2cWriteId or snmpV2cWriteDescription: # All snmpV2cWrite details from the Cisco DNA Center @@ -1968,9 +2027,9 @@ def get_want_assign_credentials(self, AssignCredentials): return self want.get("assign_credentials").update({"snmpV2WriteId": snmpV2cWriteDetail.get("id")}) - httpReadId = AssignCredentials.get("http_read") - httpReadDescription = AssignCredentials.get("http_read_description") - httpReadUsername = AssignCredentials.get("http_read_username") + httpReadId = AssignCredentials.get("https_read").get("id") + httpReadDescription = AssignCredentials.get("https_read").get("description") + httpReadUsername = AssignCredentials.get("https_read").get("username") if httpReadId or httpReadDescription and httpReadUsername: # All httpRead details from the Cisco DNA Center @@ -1997,9 +2056,9 @@ def get_want_assign_credentials(self, AssignCredentials): return self want.get("assign_credentials").update({"httpRead": httpReadDetail.get("id")}) - httpWriteId = AssignCredentials.get("http_write") - httpWriteDescription = AssignCredentials.get("http_write_description") - httpWriteUsername = AssignCredentials.get("http_write_username") + httpWriteId = AssignCredentials.get("https_write").get("id") + httpWriteDescription = AssignCredentials.get("https_write").get("description") + httpWriteUsername = AssignCredentials.get("https_write").get("username") if httpWriteId or httpWriteDescription and httpWriteUsername: # All httpWrite details from the Cisco DNA Center @@ -2026,8 +2085,8 @@ def get_want_assign_credentials(self, AssignCredentials): return self want.get("assign_credentials").update({"httpWrite": httpWriteDetail.get("id")}) - snmpV3Id = AssignCredentials.get("snmp_v3_id") - snmpV3Description = AssignCredentials.get("snmp_v3_description") + snmpV3Id = AssignCredentials.get("snmp_v3").get("id") + snmpV3Description = AssignCredentials.get("snmp_v3").get("description") if snmpV3Id or snmpV3Description: # All snmpV3 details from the Cisco DNA Center diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index ca0f661b55..4f036baf6b 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -580,7 +580,7 @@ description: Import the project details. type: dict suboptions: - doVersion: + do_version: description: DoVersion query parameter. If this flag is true, creates a new version of the template with the imported contents, if the templates already exists. " If false and if template already exists, then operation @@ -590,7 +590,7 @@ description: Import the template details. type: dict suboptions: - doVersion: + do_version: description: DoVersion query parameter. If this flag is true, creates a new version of the template with the imported contents, if the templates already exists. " If false and if template already exists, then operation @@ -606,7 +606,7 @@ composite: description: Is it composite template. type: bool - containingTemplates: + containing_templates: description: Configuration Template Import Template's containingTemplates. elements: dict suboptions: @@ -620,13 +620,13 @@ description: Configuration Template Import Template's deviceTypes. elements: dict suboptions: - productFamily: + product_family: description: Device family. type: str - productSeries: + product_series: description: Device series. type: str - productType: + product_type: description: Device type. type: str type: list @@ -642,26 +642,26 @@ project_name: description: Project name. type: str - rollbackTemplateParams: + rollback_template_params: description: Configuration Template Import Template's rollbackTemplateParams. elements: dict suboptions: binding: description: Bind to source. type: str - customOrder: + custom_order: description: CustomOrder of template param. type: int - dataType: + data_type: description: Datatype of template param. type: str - defaultValue: + default_value: description: Default value of template param. type: str description: description: Description of template param. type: str - displayName: + display_name: description: Display name of param. type: str group: @@ -670,22 +670,22 @@ id: description: UUID of template param. type: str - instructionText: + instruction_text: description: Instruction text for param. type: str key: description: Key. type: str - notParam: + not_param: description: Is it not a variable. type: bool order: description: Order of template param. type: int - paramArray: + param_array: description: Is it an array. type: bool - parameterName: + parameter_name: description: Name of template param. type: str provider: @@ -698,10 +698,10 @@ id: description: UUID of range. type: str - maxValue: + max_value: description: Max value of range. type: int - minValue: + min_value: description: Min value of range. type: int type: list @@ -711,17 +711,17 @@ selection: description: Configuration Template Import Template's selection. suboptions: - defaultSelectedValues: + default_selected_values: description: Default selection values. elements: str type: list id: description: UUID of selection. type: str - selectionType: + selection_type: description: Type of selection(SINGLE_SELECT or MULTI_SELECT). type: str - selectionValues: + selection_values: description: Selection values. type: dict type: dict @@ -740,26 +740,26 @@ template_content: description: Template content. type: str - templateParams: + template_params: description: Configuration Template Import Template's templateParams. elements: dict suboptions: binding: description: Bind to source. type: str - customOrder: + custom_order: description: CustomOrder of template param. type: int - dataType: + data_type: description: Datatype of template param. type: str - defaultValue: + default_value: description: Default value of template param. type: str description: description: Description of template param. type: str - displayName: + display_name: description: Display name of param. type: str group: @@ -768,22 +768,22 @@ id: description: UUID of template param. type: str - instructionText: + instruction_text: description: Instruction text for param. type: str key: description: Key. type: str - notParam: + not_param: description: Is it not a variable. type: bool order: description: Order of template param. type: int - paramArray: + param_array: description: Is it an array. type: bool - parameterName: + parameter_name: description: Name of template param. type: str provider: @@ -796,10 +796,10 @@ id: description: UUID of range. type: str - maxValue: + max_value: description: Max value of range. type: int - minValue: + min_value: description: Min value of range. type: int type: list @@ -809,17 +809,17 @@ selection: description: Configuration Template Import Template's selection. suboptions: - defaultSelectedValues: + default_selected_values: description: Default selection values. elements: str type: list id: description: UUID of selection. type: str - selectionType: + selection_type: description: Type of selection(SINGLE_SELECT or MULTI_SELECT). type: str - selectionValues: + selection_values: description: Selection values. type: dict type: dict @@ -828,10 +828,10 @@ description: Current version of template. type: str type: list - createTime: + create_time: description: Create time of template. type: int - customParamsOrder: + custom_params_order: description: Custom Params Order. type: bool description: @@ -841,17 +841,17 @@ description: Configuration Template Import Template's deviceTypes. elements: dict suboptions: - productFamily: + product_family: description: Device family. type: str - productSeries: + product_series: description: Device series. type: str - productType: + product_type: description: Device type. type: str type: list - failurePolicy: + failure_policy: description: Define failure policy if template provisioning fails. type: str id: @@ -860,47 +860,47 @@ language: description: Template language (JINJA or VELOCITY). type: str - lastUpdateTime: + last_update_time: description: Update time of template. type: int - latestVersionTime: + latest_version_time: description: Latest versioned template time. type: int name: description: Name of template. type: str - parentTemplateId: + parent_template_id: description: Parent templateID. type: str - projectId: + project_id: description: Project UUID. type: str project_name: description: Project name. type: str - rollbackTemplateContent: + rollback_template_content: description: Rollback template content. type: str - rollbackTemplateParams: + rollback_template_params: description: Configuration Template Import Template's rollbackTemplateParams. elements: dict suboptions: binding: description: Bind to source. type: str - customOrder: + custom_order: description: CustomOrder of template param. type: int - dataType: + data_type: description: Datatype of template param. type: str - defaultValue: + default_value: description: Default value of template param. type: str description: description: Description of template param. type: str - displayName: + display_name: description: Display name of param. type: str group: @@ -909,22 +909,22 @@ id: description: UUID of template param. type: str - instructionText: + instruction_text: description: Instruction text for param. type: str key: description: Key. type: str - notParam: + not_param: description: Is it not a variable. type: bool order: description: Order of template param. type: int - paramArray: + param_array: description: Is it an array. type: bool - parameterName: + parameter_name: description: Name of template param. type: str provider: @@ -937,10 +937,10 @@ id: description: UUID of range. type: str - maxValue: + max_value: description: Max value of range. type: int - minValue: + min_value: description: Min value of range. type: int type: list @@ -950,17 +950,17 @@ selection: description: Configuration Template Import Template's selection. suboptions: - defaultSelectedValues: + default_selected_values: description: Default selection values. elements: str type: list id: description: UUID of selection. type: str - selectionType: + selection_type: description: Type of selection(SINGLE_SELECT or MULTI_SELECT). type: str - selectionValues: + selection_values: description: Selection values. type: dict type: dict @@ -971,7 +971,7 @@ software_variant: description: Applicable device software variant. type: str - softwareVersion: + software_version: description: Applicable device software version. type: str tags: @@ -988,26 +988,26 @@ template_content: description: Template content. type: str - templateParams: + template_params: description: Configuration Template Import Template's templateParams. elements: dict suboptions: binding: description: Bind to source. type: str - customOrder: + custom_order: description: CustomOrder of template param. type: int - dataType: + data_type: description: Datatype of template param. type: str - defaultValue: + default_value: description: Default value of template param. type: str description: description: Description of template param. type: str - displayName: + display_name: description: Display name of param. type: str group: @@ -1016,22 +1016,22 @@ id: description: UUID of template param. type: str - instructionText: + instruction_text: description: Instruction text for param. type: str key: description: Key. type: str - notParam: + not_param: description: Is it not a variable. type: bool order: description: Order of template param. type: int - paramArray: + param_array: description: Is it an array. type: bool - parameterName: + parameter_name: description: Name of template param. type: str provider: @@ -1044,10 +1044,10 @@ id: description: UUID of range. type: str - maxValue: + max_value: description: Max value of range. type: int - minValue: + min_value: description: Min value of range. type: int type: list @@ -1057,34 +1057,34 @@ selection: description: Configuration Template Import Template's selection. suboptions: - defaultSelectedValues: + default_selected_values: description: Default selection values. elements: str type: list id: description: UUID of selection. type: str - selectionType: + selection_type: description: Type of selection(SINGLE_SELECT or MULTI_SELECT). type: str - selectionValues: + selection_values: description: Selection values. type: dict type: dict type: list - validationErrors: + validation_errors: description: Configuration Template Import Template's validationErrors. suboptions: - rollbackTemplateErrors: + rollback_template_errors: description: Validation or design conflicts errors of rollback template. type: dict - templateErrors: + template_errors: description: Validation or design conflicts errors. type: dict - templateId: + template_id: description: UUID of template. type: str - templateVersion: + template_version: description: Current version of template. type: str type: dict @@ -1399,36 +1399,36 @@ def validate_input(self): 'tags': {'type': 'list'}, 'author': {'type': 'str'}, 'composite': {'type': 'bool'}, - 'containingTemplates': {'type': 'list'}, - 'createTime': {'type': 'int'}, - 'customParamsOrder': {'type': 'bool'}, + 'containing_templates': {'type': 'list'}, + 'create_time': {'type': 'int'}, + 'custom_params_order': {'type': 'bool'}, 'description': {'type': 'str'}, 'device_types': { 'type': 'list', 'elements': 'dict', - 'productFamily': {'type': 'str'}, - 'productSeries': {'type': 'str'}, - 'productType': {'type': 'str'}, + 'product_family': {'type': 'str'}, + 'product_series': {'type': 'str'}, + 'product_type': {'type': 'str'}, }, - 'failurePolicy': {'type': 'str'}, + 'failure_policy': {'type': 'str'}, 'id': {'type': 'str'}, 'language': {'type': 'str'}, - 'lastUpdateTime': {'type': 'int'}, - 'latestVersionTime': {'type': 'int'}, + 'last_update_time': {'type': 'int'}, + 'latest_version_time': {'type': 'int'}, 'name': {'type': 'str'}, - 'parentTemplateId': {'type': 'str'}, - 'projectId': {'type': 'str'}, + 'parent_template_id': {'type': 'str'}, + 'project_id': {'type': 'str'}, 'project_name': {'type': 'str'}, 'project_description': {'type': 'str'}, - 'rollbackTemplateContent': {'type': 'str'}, - 'rollbackTemplateParams': {'type': 'list'}, + 'rollback_template_content': {'type': 'str'}, + 'rollback_template_params': {'type': 'list'}, 'software_type': {'type': 'str'}, 'software_variant': {'type': 'str'}, - 'softwareVersion': {'type': 'str'}, + 'software_version': {'type': 'str'}, 'template_content': {'type': 'str'}, - 'templateParams': {'type': 'list'}, + 'template_params': {'type': 'list'}, 'template_name': {'type': 'str'}, - 'validationErrors': {'type': 'dict'}, + 'validation_errors': {'type': 'dict'}, 'version': {'type': 'str'} } } @@ -1461,9 +1461,8 @@ def get_project_params(self, params): project_params (dict) - Organized Project parameters. """ - configuration_templates = params.get("configuration_templates") - project_params = {"name": configuration_templates.get("project_name"), - "description": configuration_templates.get("project_description") + project_params = {"name": params.get("project_name"), + "description": params.get("project_description") } return project_params @@ -1839,45 +1838,39 @@ def get_template_params(self, params): temp_params (dict) - Organized template parameters. """ - configuration_templates = params.get("configuration_templates") temp_params = { - "tags": self.get_tags(configuration_templates - .get("template_tag")), - "author": configuration_templates.get("author"), - "composite": configuration_templates.get("composite"), + "tags": self.get_tags(params.get("template_tag")), + "author": params.get("author"), + "composite": params.get("composite"), "containingTemplates": - self.get_containing_templates( - configuration_templates.get("containing_templates") - ), - "createTime": configuration_templates.get("create_time"), - "customParamsOrder": configuration_templates.get("custom_params_order"), - "description": configuration_templates.get("template_description"), + self.get_containing_templates(params.get("containing_templates")), + "createTime": params.get("create_time"), + "customParamsOrder": params.get("custom_params_order"), + "description": params.get("template_description"), "deviceTypes": - self.get_device_types(configuration_templates.get("device_types")), - "failurePolicy": configuration_templates.get("failure_policy"), - "id": configuration_templates.get("id"), - "language": configuration_templates.get("language").upper(), - "lastUpdateTime": configuration_templates.get("last_update_time"), - "latestVersionTime": configuration_templates.get("latest_version_time"), - "name": configuration_templates.get("template_name"), - "parentTemplateId": configuration_templates.get("parent_template_id"), - "projectId": configuration_templates.get("project_id"), - "projectName": configuration_templates.get("project_name"), - "rollbackTemplateContent": configuration_templates.get("rollback_template_content"), + self.get_device_types(params.get("device_types")), + "failurePolicy": params.get("failure_policy"), + "id": params.get("id"), + "language": params.get("language").upper(), + "lastUpdateTime": params.get("last_update_time"), + "latestVersionTime": params.get("latest_version_time"), + "name": params.get("template_name"), + "parentTemplateId": params.get("parent_template_id"), + "projectId": params.get("project_id"), + "projectName": params.get("project_name"), + "rollbackTemplateContent": params.get("rollback_template_content"), "rollbackTemplateParams": - self.get_template_info(configuration_templates.get("rollback_template_params")), - "softwareType": configuration_templates.get("software_type"), - "softwareVariant": configuration_templates.get("software_variant"), - "softwareVersion": configuration_templates.get("software_version"), - "templateContent": configuration_templates.get("template_content"), + self.get_template_info(params.get("rollback_template_params")), + "softwareType": params.get("software_type"), + "softwareVariant": params.get("software_variant"), + "softwareVersion": params.get("software_version"), + "templateContent": params.get("template_content"), "templateParams": - self.get_template_info( - configuration_templates.get("template_params") - ), + self.get_template_info(params.get("template_params")), "validationErrors": - self.get_validation_errors(configuration_templates.get("validation_errors")), - "version": configuration_templates.get("version"), - "project_id": configuration_templates.get("project_id") + self.get_validation_errors(params.get("validation_errors")), + "version": params.get("version"), + "project_id": params.get("project_id") } self.log(str(temp_params)) copy_temp_params = copy.deepcopy(temp_params) @@ -2071,8 +2064,8 @@ def get_want(self, config): want = {} configuration_templates = config.get("configuration_templates") if configuration_templates: - template_params = self.get_template_params(config) - project_params = self.get_project_params(config) + template_params = self.get_template_params(configuration_templates) + project_params = self.get_project_params(configuration_templates) version_comments = configuration_templates.get("version_description") if self.params.get("state") == "merged": @@ -2322,103 +2315,7 @@ def get_export_template_values(self, export_values): self.status = "success" return self - def export_project_or_template(self, export): - """ - Export templates and projects in DNAC with fields provided in DNAC. - - Parameters: - export (dict) - Playbook details containing export information. - - Returns: - None - """ - - export_project = export.get("project") - self.log(str(export_project)) - if export_project: - response = self.dnac._exec( - family="configuration_templates", - function='export_projects', - params={"payload": export_project}, - ) - validation_string = "successfully exported project" - self.check_task_response_status(response, validation_string, True).check_return_status() - self.result['response'][0].update({"exportProject": self.msg}) - - export_values = export.get("template") - if export_values: - self.get_export_template_values(export_values).check_return_status() - self.log(str(self.export_template)) - response = self.dnac._exec( - family="configuration_templates", - function='export_templates', - params={"payload": self.export_template}, - ) - validation_string = "successfully exported template" - self.check_task_response_status(response, validation_string, True).check_return_status() - self.result['response'][0].update({"exportTemplate": self.msg}) - - def import_project_or_template(self, _import): - """ - Import templates and projects in DNAC with fields provided in DNAC. - - Parameters: - _import (dict) - Playbook details containing import information. - - Returns: - None - """ - - do_version = _import.get("project").get("do_version") - payload = None - if _import.get("project").get("payload"): - payload = _import.get("project").get("payload") - else: - self.msg = "Mandatory parameter payload is not found under import project" - self.status = "failed" - return self - _import_project = { - "do_version": do_version, - # "payload": "{0}".format(payload) - "payload": payload - } - self.log(str(_import_project)) - if _import_project: - response = self.dnac._exec( - family="configuration_templates", - function='imports_the_projects_provided', - params=_import_project, - ) - validation_string = "successfully imported project" - self.check_task_response_status(response, validation_string).check_return_status() - self.result['response'][0].update({"importProject": validation_string}) - - _import_template = _import.get("template") - if _import_template: - self.msg = "Mandatory paramter template is not found" - self.status = "failed" - return self - if _import_template.get("projectName"): - self.msg = "Mandatory paramter project_name is not found under import template" - self.status = "failed" - return self - if _import_template.get("payload"): - self.msg = "Mandatory paramter payload is not found under import template" - self.status = "failed" - return self - - self.log(str(_import_template)) - if _import_template: - response = self.dnac._exec( - family="configuration_templates", - function='imports_the_templates_provided', - params=_import_template, - ) - validation_string = "successfully imported template" - self.check_task_response_status(response, validation_string).check_return_status() - self.result['response'][0].update({"importTemplate": validation_string}) - - def get_diff_merged(self, config): + def update_configuration_templates(self, config): """ Update/Create templates and projects in DNAC with fields provided in DNAC. @@ -2465,8 +2362,10 @@ def get_diff_merged(self, config): self.log("Updating Existing Template") else: # Template does not need update - self.result['response'] = self.have_template.get("template") - self.result['msg'] = "Template does not need update" + self.result.update({ + 'response': self.have_template.get("template"), + 'msg': "Template does not need update" + }) self.status = "exited" return self else: @@ -2489,7 +2388,6 @@ def get_diff_merged(self, config): op_modifies=True, params=version_params ) - task_details = {} task_id = response.get("response").get("taskId") if not task_id: self.msg = "Task id: {0} not found".format(task_id) @@ -2507,14 +2405,138 @@ def get_diff_merged(self, config): self.status = "failed" return self + def handle_export(self, config): + """ + Export templates and projects in DNAC with fields provided in DNAC. + + Parameters: + config (dict) - Playbook details containing template information. + + Returns: + self + """ + export = config.get("export") if export: - self.export_project_or_template(export) + export_project = export.get("project") + self.log(str(export_project)) + if export_project: + response = self.dnac._exec( + family="configuration_templates", + function='export_projects', + params={"payload": export_project}, + ) + validation_string = "successfully exported project" + self.check_task_response_status(response, validation_string, True).check_return_status() + self.result['response'][0].update({"exportProject": self.msg}) + + export_values = export.get("template") + if export_values: + self.get_export_template_values(export_values).check_return_status() + self.log(str(self.export_template)) + response = self.dnac._exec( + family="configuration_templates", + function='export_templates', + params={"payload": self.export_template}, + ) + validation_string = "successfully exported template" + self.check_task_response_status(response, validation_string, True).check_return_status() + self.result['response'][0].update({"exportTemplate": self.msg}) + + return self + + def handle_import(self, config): + """ + Import templates and projects in DNAC with fields provided in DNAC. + + Parameters: + config (dict) - Playbook details containing template information. + + Returns: + self + """ _import = config.get("import") if _import: # _import_project = _import.get("project") - self.import_project_or_template(_import) + do_version = _import.get("project").get("do_version") + payload = None + if _import.get("project").get("payload"): + payload = _import.get("project").get("payload") + else: + self.msg = "Mandatory parameter payload is not found under import project" + self.status = "failed" + return self + _import_project = { + "doVersion": do_version, + # "payload": "{0}".format(payload) + "payload": payload + } + self.log(str(_import_project)) + if _import_project: + response = self.dnac._exec( + family="configuration_templates", + function='imports_the_projects_provided', + params=_import_project, + ) + validation_string = "successfully imported project" + self.check_task_response_status(response, validation_string).check_return_status() + self.result['response'][0].update({"importProject": validation_string}) + + _import_template = _import.get("template") + if _import_template.get("project_name"): + self.msg = "Mandatory paramter project_name is not found under import template" + self.status = "failed" + return self + if _import_template.get("payload"): + self.msg = "Mandatory paramter payload is not found under import template" + self.status = "failed" + return self + + payload = _import_template.get("project_name") + import_template = { + "doVersion": _import_template.get("do_version"), + "projectName": _import_template.get("project_name"), + "payload": self.get_template_params(payload) + } + self.log(str(_import_template)) + if _import_template: + response = self.dnac._exec( + family="configuration_templates", + function='imports_the_templates_provided', + params=import_template, + ) + validation_string = "successfully imported template" + self.check_task_response_status(response, validation_string).check_return_status() + self.result['response'][0].update({"importTemplate": validation_string}) + + return self + + def get_diff_merged(self, config): + """ + Update/Create templates and projects in DNAC with fields provided in DNAC. + Export the tempaltes and projects. + Import the templates and projects. + Check using check_return_status(). + + Parameters: + config (dict) - Playbook details containing template information. + + Returns: + self + """ + + self.update_configuration_templates(config) + if self.status == "failed": + return self # we can collect the return status in caller and do needed things. + + self.handle_export(config) + if self.status == "failed": + return self # we can collect the return status in caller and do needed things. + + self.handle_import(config) + if self.status == "failed": + return self # we can collect the return status in caller and do needed things. self.msg = "Successfully completed merged state execution" self.status = "success" From 8c39d7e36af5770ca124449baed26596469da26b Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Sun, 10 Dec 2023 01:24:14 +0530 Subject: [PATCH 27/50] Addressed PR comments --- plugins/modules/device_credential_intent.py | 2 +- plugins/modules/template_intent.py | 52 ++++++++++----------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index 9944119d5e..76e08880e8 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -5,7 +5,6 @@ """Ansible module to perform operations on device credentials in Cisco DNA Center.""" from __future__ import absolute_import, division, print_function -import string __metaclass__ = type __author__ = ['Muthu Rakesh, Madhan Sankaranarayanan'] @@ -687,6 +686,7 @@ """ import copy +import string from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( DnacBase, diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index 4f036baf6b..97c4a69818 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -1141,9 +1141,9 @@ custom_params_order: true description: string device_types: - - productFamily: string - productSeries: string - productType: string + - product_family: string + product_series: string + product_type: string failure_policy: string id: string language: string @@ -1163,12 +1163,12 @@ name: string template_content: string validation_errors: - rollbackTemplateErrors: + rollback_template_errors: - {} - templateErrors: + template_errors: - {} - templateId: string - templateVersion: string + template_id: string + template_version: string version: string export: project: @@ -1178,38 +1178,38 @@ template_name: string import: project: - doVersion: true + do_version: true export: - doVersion: true + do_version: true payload: - author: string composite: true - containingTemplates: + containing_templates: - composite: true description: string device_types: - - productFamily: string - productSeries: string - productType: string + - product_family: string + product_series: string + product_type: string id: string language: string name: string project_name: string - rollbackTemplateParams: + rollback_template_params: - binding: string - customOrder: 0 - dataType: string - defaultValue: string + custom_order: 0 + data_type: string + default_value: string description: string - displayName: string + display_name: string group: string id: string - instructionText: string + instruction_text: string key: string - notParam: true + not_param: true order: 0 - paramArray: true - parameterName: string + param_array: true + parameter_name: string provider: string range: - id: string @@ -2526,17 +2526,17 @@ def get_diff_merged(self, config): self """ - self.update_configuration_templates(config) + self.update_configuration_templates(config) if self.status == "failed": - return self # we can collect the return status in caller and do needed things. + return self self.handle_export(config) if self.status == "failed": - return self # we can collect the return status in caller and do needed things. + return self self.handle_import(config) if self.status == "failed": - return self # we can collect the return status in caller and do needed things. + return self self.msg = "Successfully completed merged state execution" self.status = "success" From 3c4b7c28f92a1e810991cd585acc1776461690ef Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Sun, 10 Dec 2023 01:30:56 +0530 Subject: [PATCH 28/50] Addressed PR comments --- plugins/modules/device_credential_intent.py | 1 - plugins/modules/template_intent.py | 8 ++++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index 76e08880e8..f7816cf7fa 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -686,7 +686,6 @@ """ import copy -import string from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( DnacBase, diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index 97c4a69818..4335955a01 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -2427,7 +2427,9 @@ def handle_export(self, config): params={"payload": export_project}, ) validation_string = "successfully exported project" - self.check_task_response_status(response, validation_string, True).check_return_status() + self.check_task_response_status(response, + validation_string, + True).check_return_status() self.result['response'][0].update({"exportProject": self.msg}) export_values = export.get("template") @@ -2440,7 +2442,9 @@ def handle_export(self, config): params={"payload": self.export_template}, ) validation_string = "successfully exported template" - self.check_task_response_status(response, validation_string, True).check_return_status() + self.check_task_response_status(response, + validation_string, + True).check_return_status() self.result['response'][0].update({"exportTemplate": self.msg}) return self From 6c6b6a01ceb1b9df62670d2eab48a62a446be656 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Mon, 11 Dec 2023 10:39:27 +0530 Subject: [PATCH 29/50] Addressed the PR comments --- playbooks/network_settings_intent.yml | 12 +++---- plugins/modules/network_settings_intent.py | 40 ++++++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/playbooks/network_settings_intent.yml b/playbooks/network_settings_intent.yml index 14062f32c7..8f803522ce 100644 --- a/playbooks/network_settings_intent.yml +++ b/playbooks/network_settings_intent.yml @@ -21,11 +21,11 @@ config: - global_pool_details: settings: - ippool: - - ip_pool_name: Global_Pool2 + ip_pool: + - name: Global_Pool2 gateway: "" #use this for updating ip_address_space: IPv6 #required when we are creating - ip_pool_cidr: 2001:db8::/64 #required when we are creating + cidr: 2001:db8::/64 #required when we are creating type: Generic dhcp_server_ips: [] #use this for updating dns_server_ips: [] #use this for updating @@ -98,10 +98,8 @@ config: - global_pool_details: settings: - ippool: - - ip_pool_name: Global_Pool2 + ip_pool: + - name: Global_Pool2 reserve_pool_details: name: IP_Pool_3 site_name: Global/Chennai/Trill - - diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index 5440df80de..6712fbc458 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -45,7 +45,7 @@ description: Global Pool's settings. type: dict suboptions: - ippool: + ip_pool: description: Global Pool's ippool. elements: dict type: list @@ -64,13 +64,13 @@ ip_address_space: description: Ip address space. type: str - ip_pool_cidr: + cidr: description: Ip pool cidr. type: str prev_name: description: previous name. type: str - ip_pool_name: + name: description: Ip Pool Name. type: str reserve_pool_details: @@ -308,11 +308,11 @@ config: - global_pool_details: settings: - ippool: - - ip_pool_name: string + ip_pool: + - name: string gateway: string ip_address_space: string - ip_pool_cidr: string + cidr: string type: Generic dhcp_server_ips: list dns_server_ips: list @@ -445,14 +445,14 @@ def validate_input(self): "type": 'dict', "settings": { "type": 'dict', - "ippool": { + "ip_pool": { "type": 'list', "ip_address_space": {"type": 'string'}, "dhcp_server_ips": {"type": 'list'}, "dns_server_ips": {"type": 'list'}, "gateway": {"type": 'string'}, - "ip_pool_cidr": {"type": 'string'}, - "ip_pool_name": {"type": 'string'}, + "cidr": {"type": 'string'}, + "name": {"type": 'string'}, "prevName": {"type": 'string'}, } } @@ -768,7 +768,9 @@ def get_network_params(self, site_id): network_aaa2 = get_dict_result(all_network_details, "key", "aaa.network.server.2") network_aaa_pan = get_dict_result(all_network_details, "key", "aaa.server.pan.network") clientAndEndpoint_aaa = get_dict_result(all_network_details, "key", "aaa.endpoint.server.1") - clientAndEndpoint_aaa2 = get_dict_result(all_network_details, "key", "aaa.endpoint.server.2") + clientAndEndpoint_aaa2 = get_dict_result(all_network_details, + "key", + "aaa.endpoint.server.2") clientAndEndpoint_aaa_pan = \ get_dict_result(all_network_details, "key", "aaa.server.pan.endpoint") @@ -994,15 +996,15 @@ def get_have_global_pool(self, config): self.status = "failed" return self - global_pool_ippool = global_pool_settings.get("ippool") + global_pool_ippool = global_pool_settings.get("ip_pool") if global_pool_ippool is None: - self.msg = "ippool in global_pool_details is missing in the playbook" + self.msg = "ip_pool in global_pool_details is missing in the playbook" self.status = "failed" return self - name = global_pool_ippool[0].get("ip_pool_name") + name = global_pool_ippool[0].get("name") if name is None: - self.msg = "Mandatory Parameter ip_pool_name required" + self.msg = "Mandatory Parameter name required" self.status = "failed" return self @@ -1177,8 +1179,8 @@ def get_want_global_pool(self, global_ippool): "IpAddressSpace": global_ippool.get("ip_address_space"), "dhcpServerIps": global_ippool.get("dhcp_server_ips"), "dnsServerIps": global_ippool.get("dns_server_ips"), - "ipPoolName": global_ippool.get("ip_pool_name"), - "ipPoolCidr": global_ippool.get("ip_pool_cidr"), + "ipPoolName": global_ippool.get("name"), + "ipPoolCidr": global_ippool.get("cidr"), "gateway": global_ippool.get("gateway"), "type": global_ippool.get("type"), }] @@ -1583,7 +1585,7 @@ def get_want(self, config): """ if config.get("global_pool_details"): - global_ippool = config.get("global_pool_details").get("settings").get("ippool")[0] + global_ippool = config.get("global_pool_details").get("settings").get("ip_pool")[0] self.get_want_global_pool(global_ippool).check_return_status() if config.get("reserve_pool_details"): @@ -1612,7 +1614,7 @@ def update_global_pool(self, config): """ name = config.get("global_pool_details") \ - .get("settings").get("ippool")[0].get("ip_pool_name") + .get("settings").get("ip_pool")[0].get("name") result_global_pool = self.result.get("response")[0].get("globalPool") result_global_pool.get("response").update({name: {}}) @@ -1942,7 +1944,7 @@ def get_diff_deleted(self, config): if config.get("global_pool_details") is not None: name = config.get("global_pool_details") \ - .get("settings").get("ippool")[0].get("ip_pool_name") + .get("settings").get("ip_pool")[0].get("name") self.delete_global_pool(name).check_return_status() return self From de71062a51290602027c1d7bce49fbf15d027541 Mon Sep 17 00:00:00 2001 From: Madhan Date: Mon, 11 Dec 2023 15:54:51 +0530 Subject: [PATCH 30/50] Adding docstring in discover intent module --- plugins/modules/discovery_intent.py | 222 +++++++++++++++++++++++++--- 1 file changed, 205 insertions(+), 17 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index a8e6fe10d6..f5ee6a5f3a 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -257,6 +257,20 @@ class DnacDiscovery(DnacBase): def __init__(self, module): + """ + Initialize an instance of the class. It also initializes an empty + list for 'creds_ids_list' attribute. + + Parameters: + - module: The module associated with the class instance. + + Returns: + The method does not return a value. Instead, it initializes the + following instance attributes: + - self.creds_ids_list: An empty list that will be used to store + credentials IDs. + """ + super().__init__(module) self.creds_ids_list = [] @@ -267,9 +281,6 @@ def validate_input(self): specification to ensure it adheres to the expected structure and data types. - 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: - self.msg: A message describing the validation result. @@ -288,6 +299,7 @@ def validate_input(self): self.msg = "config not available in playbook for validation" self.status = "success" return self + default_dicovery_name = 'discovery_' + str(time.time()) discovery_spec = { 'cdp_level': {'type': 'int', 'required': False, @@ -348,9 +360,31 @@ def validate_input(self): return self def get_creds_ids_list(self): + """ + Retrieve the list of credentials IDs associated with class instance. + + Returns: + The method returns the list of credentials IDs: + - self.creds_ids_list: The list of credentials IDs associated with + the class instance. + """ + return self.creds_ids_list def get_dnac_global_credentials_v2_info(self): + """ + Retrieve the global credentials information (version 2). + It applies the 'get_all_global_credentials_v2' function and extracts + the IDs of the credentials. If no credentials are found, the + function fails with a message. + + Returns: + This method does not return a value. However, updates the attributes: + - self.creds_ids_list: The list of credentials IDs is extended with + the IDs extracted from the response. + - self.result: A dictionary that is updated with the credentials IDs. + """ + response = self.dnac_apply['exec']( family="discovery", function='get_all_global_credentials_v2', @@ -369,14 +403,39 @@ def get_dnac_global_credentials_v2_info(self): self.result.update(dict(credential_ids=self.creds_ids_list)) def get_devices_list_info(self): + """ + Retrieve the list of devices from the validated configuration. + It then updates the result attribute with this list. + + Returns: + - devices_list: The list of devices extracted from the + 'validated_config' attribute. + """ devices_list = self.validated_config[0].get('devices_list') self.result.update(dict(devices_info=devices_list)) return devices_list def preprocessing_devices_info(self, devices_list=None): + """ + Preprocess the devices' information. Extract the IP addresses from + the list of devices and perform additional processing based on the + 'discovery_type' in the validated configuration. + + Parameters: + - devices_list: The list of devices to preprocess. If not + provided, an empty list is used. + + Returns: + - ip_address_list: If 'discovery_type' is "SINGLE", it returns the + first IP address. Otherwise, it returns a string + of IP ranges separated by commas. + """ + if devices_list is None: devices_list = [] + ip_address_list = [device['ip'] for device in devices_list] + if self.validated_config[0].get('discovery_type') == "SINGLE": ip_address_list = ip_address_list[0] else: @@ -387,11 +446,28 @@ def preprocessing_devices_info(self, devices_list=None): ) ) ip_address_list = ','.join(ip_address_list) + return ip_address_list def create_params(self, credential_ids=None, ip_address_list=None): + """ + Create a new parameter object based on the validated configuration, + credential IDs, and IP address list. + + Parameters: + - credential_ids: The list of credential IDs to include in the + parameters. If not provided, an empty list is used. + - ip_address_list: The list of IP addresses to include in the + parameters. If not provided, None is used. + + Returns: + - new_object_params: A dictionary containing the newly created + parameters. + """ + if credential_ids is None: credential_ids = [] + new_object_params = {} new_object_params['cdpLevel'] = self.validated_config[0].get('cdp_level') new_object_params['discoveryType'] = self.validated_config[0].get('discovery_type') @@ -434,11 +510,29 @@ def create_params(self, credential_ids=None, ip_address_list=None): new_object_params['snmpVersion'] = self.validated_config[0].get('snmp_version') new_object_params['timeout'] = self.validated_config[0].get('timeout') new_object_params['userNameList'] = self.validated_config[0].get('user_name_list') + return new_object_params def create_discovery(self, credential_ids=None, ip_address_list=None): + """ + Start a new discovery process in the DNA Center. It creates the + parameters required for the discovery and then calls the + 'start_discovery' function. The result of the discovery process + is added to the 'result' attribute. + + Parameters: + - credential_ids: The list of credential IDs to include in the + discovery. If not provided, an empty list is used. + - ip_address_list: The list of IP addresses to include in the + discovery. If not provided, None is used. + + Returns: + - task_id: The ID of the task created for the discovery process. + """ + if credential_ids is None: credential_ids = [] + result = self.dnac_apply['exec']( family="discovery", function="start_discovery", @@ -451,6 +545,19 @@ def create_discovery(self, credential_ids=None, ip_address_list=None): return result.response.get('taskId') def get_task_status(self, task_id=None): + """ + Monitor the status of a task in the DNA Center. It checks the task + status periodically until the task is no longer 'In Progress'. + If the task encounters an error or fails, it immediately fails the + module and returns False. + + Parameters: + - task_id: The ID of the task to monitor. + + Returns: + - result: True if the task completed successfully, False otherwise. + """ + result = False params = dict(task_id=task_id) while True: @@ -477,6 +584,15 @@ def get_task_status(self, task_id=None): return result def lookup_discovery_by_range_via_name(self): + """ + Retrieve a specific discovery by name from a range of + discoveries in the DNA Center. + + Returns: + - discovery: The discovery with the specified name from the range + of discoveries. If no matching discovery is found, it + returns None. + """ params = dict( start_index=self.validated_config[0].get("start_index"), records_to_return=self.validated_config[0].get("records_to_return"), @@ -488,6 +604,7 @@ def lookup_discovery_by_range_via_name(self): function='get_discoveries_by_range', params=params ) + return next( filter( lambda x: x['name'] == self.validated_config[0].get('discovery_name'), @@ -496,8 +613,20 @@ def lookup_discovery_by_range_via_name(self): ) def get_discoveries_by_range_until_success(self): + """ + Continuously retrieve a specific discovery by name from a range of + discoveries in the DNA Center until the discovery is complete. + + Returns: + - discovery: The completed discovery with the specified name from + the range of discoveries. If the discovery is not + found or not completed, the function fails the module + and returns None. + """ + result = False discovery = self.lookup_discovery_by_range_via_name() + if not discovery: msg = 'Cannot find any discovery task with name {0} -- Discovery result: {1}'.format( self.validated_config[0].get("discovery_name"), discovery) @@ -508,6 +637,7 @@ def get_discoveries_by_range_until_success(self): if discovery.get('discoveryCondition') == 'Complete': result = True break + time.sleep(3) if not result: @@ -519,13 +649,26 @@ def get_discoveries_by_range_until_success(self): return discovery def get_discovery_device_info(self, discovery_id=None, task_id=None): + """ + Retrieve the information of devices discovered by a specific discovery + process in the DNA Center. It checks the reachability status of the + devices periodically until all devices are reachable or until a + maximum of 3 attempts. + + Parameters: + - discovery_id: ID of the discovery process to retrieve devices from. + - task_id: ID of the task associated with the discovery process. + + Returns: + - result: True if all devices are reachable, False otherwise. + """ + params = dict( id=discovery_id, task_id=task_id, headers=self.validated_config[0].get("headers"), ) result = False - response = [] count = 0 while True: response = self.dnac_apply['exec']( @@ -537,9 +680,8 @@ def get_discovery_device_info(self, discovery_id=None, task_id=None): if all(res.get('reachabilityStatus') == 'Success' for res in devices): result = True break - else: - count += 1 - + + count += 1 if count == 3: break @@ -553,16 +695,38 @@ def get_discovery_device_info(self, discovery_id=None, task_id=None): return result def get_exist_discovery(self): + """ + Retrieve an existing discovery by its name from a range of discoveries. + + Returns: + - discovery: The discovery with the specified name from the range of + discoveries. If no matching discovery is found, it + returns None and updates the 'exist_discovery' entry in + the result dictionary to None. + """ + discovery = self.lookup_discovery_by_range_via_name() if not discovery: self.result.update(dict(exist_discovery=discovery)) return None + have = dict(exist_discovery=discovery) self.have = have self.result.update(dict(exist_discovery=discovery)) return discovery def delete_exist_discovery(self, params): + """ + Delete an existing discovery in the DNA Center by its ID. + + Parameters: + - params: A dictionary containing the parameters for the delete + operation, including the ID of the discovery to delete. + + Returns: + - task_id: The ID of the task created for the delete operation. + """ + response = self.dnac_apply['exec']( family="discovery", function="delete_discovery_by_id", @@ -572,6 +736,16 @@ def delete_exist_discovery(self, params): return response.response.get('taskId') def get_diff_merged(self): + """ + Retrieve the information of devices discovered by a specific discovery + process in the DNA Center, delete existing discoveries if they exist, + and create a new discovery. The function also updates various + attributes of the class instance. + + Returns: + - self: The instance of the class with updated attributes. + """ + self.get_dnac_global_credentials_v2_info() devices_list_info = self.get_devices_list_info() ip_address_list = self.preprocessing_devices_info(devices_list_info) @@ -580,8 +754,10 @@ def get_diff_merged(self): params = dict(id=exist_discovery.get('id')) discovery_task_id = self.delete_exist_discovery(params=params) complete_discovery = self.get_task_status(task_id=discovery_task_id) + discovery_task_id = self.create_discovery( - credential_ids=self.get_creds_ids_list(), ip_address_list=ip_address_list) + credential_ids=self.get_creds_ids_list(), + ip_address_list=ip_address_list) complete_discovery = self.get_task_status(task_id=discovery_task_id) discovery_task_info = self.get_discoveries_by_range_until_success() result = self.get_discovery_device_info(discovery_id=discovery_task_info.get('id')) @@ -593,18 +769,30 @@ def get_diff_merged(self): return self def get_diff_deleted(self): + """ + Delete an existing discovery in the DNA Center by its name, and + updates various attributes of the class instance. If no + discovery with the specified name is found, the function + updates the 'msg' attribute with an appropriate message. + + Returns: + - self: The instance of the class with updated attributes. + """ + exist_discovery = self.get_exist_discovery() - if exist_discovery: - params = dict(id=exist_discovery.get('id')) - discovery_task_id = self.delete_exist_discovery(params=params) - complete_discovery = self.get_task_status(task_id=discovery_task_id) - self.result["changed"] = True - self.result['msg'] = "Discovery Deleted Successfully" - self.result['diff'] = self.validated_config - self.result['response'] = discovery_task_id - else: + if not exist_discovery: self.result['msg'] = "Discovery {0} Not Found".format( self.validated_config[0].get("discovery_name")) + return self + + params = dict(id=exist_discovery.get('id')) + discovery_task_id = self.delete_exist_discovery(params=params) + complete_discovery = self.get_task_status(task_id=discovery_task_id) + self.result["changed"] = True + self.result['msg'] = "Discovery Deleted Successfully" + self.result['diff'] = self.validated_config + self.result['response'] = discovery_task_id + return self From aab6e3381681cdaf90c55f9cabe1cbc44ee98cfe Mon Sep 17 00:00:00 2001 From: Madhan Date: Mon, 11 Dec 2023 15:59:26 +0530 Subject: [PATCH 31/50] Removing while spaces --- plugins/modules/discovery_intent.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/discovery_intent.py b/plugins/modules/discovery_intent.py index f5ee6a5f3a..d1a22d800d 100644 --- a/plugins/modules/discovery_intent.py +++ b/plugins/modules/discovery_intent.py @@ -263,7 +263,7 @@ def __init__(self, module): Parameters: - module: The module associated with the class instance. - + Returns: The method does not return a value. Instead, it initializes the following instance attributes: @@ -373,7 +373,7 @@ def get_creds_ids_list(self): def get_dnac_global_credentials_v2_info(self): """ - Retrieve the global credentials information (version 2). + Retrieve the global credentials information (version 2). It applies the 'get_all_global_credentials_v2' function and extracts the IDs of the credentials. If no credentials are found, the function fails with a message. @@ -680,7 +680,7 @@ def get_discovery_device_info(self, discovery_id=None, task_id=None): if all(res.get('reachabilityStatus') == 'Success' for res in devices): result = True break - + count += 1 if count == 3: break From e426eed53a471c60889b57cf636c3dedd37d07ab Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 12 Dec 2023 14:29:57 +0530 Subject: [PATCH 32/50] provsion wired device, reboot AP's, export device list, delete provsion devices --- plugins/modules/inventory_intent.py | 396 +++++++++++++++++++++++++--- 1 file changed, 358 insertions(+), 38 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 3cda5fc210..45dc1899d1 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -78,6 +78,7 @@ type: str password: description: Device's password. Required for Adding Network Device. + Also needed for file encryption while exporting device in a csv file. type: str serial_number: description: Device's serial number. @@ -176,6 +177,15 @@ description: Preview/Deploy [Preview means the configuration is not pushed to the device. Deploy makes the configuration pushed to the device] type: str default: "Deploy" + site_name: + description: Required for Provisioning of Wired and Wireless Devices. + type: str + operation_enum: + description: enum(CREDENTIALDETAILS, DEVICEDETAILS) 0 to export Device Credential Details Or 1 to export Device Details. + type: str + parameters: + description: List of device parameters that needs to be exported to file. + type: str requirements: @@ -338,6 +348,22 @@ device_resync: false type: "THIRD_PARTY_DEVICE" +- name: Associate Wired Devices to site and Provisioned it in Inventory + cisco.dnac.inventory_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: False + state: merged + config: + - ip_address: string + provision_wired_device: + site_name: "{{item.site_name}}" + - name: Update Device Role with IP Address cisco.dnac.inventory_intent: dnac_host: "{{dnac_host}}" @@ -371,13 +397,28 @@ - ip_address: string device_updated: true update_interface_details: - - ip_address: "{{item.ip_address}}" - device_updated: "{{item.device_updated}}" - update_interface_details: - description: str - admin_status: str - vlan_id: int - voice_vlan_id: int + description: str + admin_status: str + vlan_id: int + voice_vlan_id: int + +- name: Export Device Details in a CSV file Interface details with IP Address + cisco.dnac.inventory_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: False + state: merged + config: + - ip_address: string + export_device_list: + password: str + operation_enum: str + parameters: str - name: Create Global User Defined with IP Address cisco.dnac.inventory_intent: @@ -413,6 +454,21 @@ device_resync: True force_sync: False +- name: Reboot AP Devices with IP Addresses + cisco.dnac.inventory_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: False + state: merged + config: + - ip_address: string + reboot_device: True + - name: Delete Device by id cisco.dnac.inventory_intent: dnac_host: "{{dnac_host}}" @@ -425,7 +481,8 @@ dnac_log: False state: deleted config: - - clean_config: false + - ip_address: string + clean_config: false id: string - name: Delete Global User Defined Field with name @@ -462,7 +519,9 @@ } """ - +import csv +from datetime import datetime +from io import StringIO from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( DnacBase, @@ -483,7 +542,7 @@ def validate_input(self): Validate the fields provided in the playbook. Checks the configuration provided in the playbook against a predefined specification to ensure it adheres to the expected structure and data types. - Args: + Paramters: 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: @@ -529,7 +588,8 @@ def validate_input(self): 'clean_config': {'type': 'bool'}, 'add_user_defined_field': {'type': 'dict'}, 'upate_interface_details': {'type': 'dict'}, - 'deployment_mode': {'default': 'Deploy', 'type': 'str'} + 'deployment_mode': {'default': 'Deploy', 'type': 'str'}, + 'provision_wired_device': {'type': 'dict'} } # Validate device params @@ -554,7 +614,7 @@ def validate_input(self): def device_exists_in_dnac(self): """ Check which devices already exists in Cisco DNA Center and return both device_exist and device_not_exist in dnac. - Args: + Paramters: self (object): An instance of a class used for interacting with Cisco Cisco DNA Center. Returns: list: A list of devices that exist in Cisco DNA Center. @@ -591,7 +651,7 @@ def device_exists_in_dnac(self): def is_udf_exist(self, field_name): """ Check if a Global User Defined Field exists in Cisco DNA Center based on its name. - Args: + Paramters: self (object): An instance of a class used for interacting with Cisco DNA Center. field_name (str): The name of the Global User Defined Field. Returns: @@ -621,7 +681,7 @@ def is_udf_exist(self, field_name): def create_user_defined_field(self): """ Create a Global User Defined Field in Cisco DNA Center based on the provided configuration. - Args: + Paramters: self (object): An instance of a class used for interacting with Cisco DNA Center. Returns: self (object): An instance of a class used for interacting with Cisco DNA Center. @@ -651,7 +711,7 @@ def create_user_defined_field(self): def add_field_to_devices(self, device_ids): """ Add a Global user-defined field with specified details to a list of devices in Cisco DNA Center. - Args: + Paramters: self (object): An instance of a class used for interacting with Cisco DNA Center. device_ids (list): A list of device IDs to which the user-defined field will be added. Returns: @@ -691,10 +751,78 @@ def add_field_to_devices(self, device_ids): return self + def provisioned_wired_device(self): + """ + Provision wired devices in Cisco DNA Center. + Paramters: + self (object): An instance of a class used for interacting with Cisco DNA Center. + Returns: + self (object): An instance of the class with updated result, status, and log. + Description: + This function provisions wired devices in Cisco DNA Center based on the configuration provided. + It retrieves the site name and IP addresses of the devices from the configuration, + attempts to provision each device, and monitors the provisioning process. + """ + + site_name = self.config[0].get('provision_wired_device').get("site_name") + device_ips = self.config[0].get("ip_address") + provision_count = 0 + + for device_ip in device_ips: + try: + provision_wired_params = { + 'deviceManagementIpAddress': device_ip, + 'siteNameHierarchy': site_name, + } + response = self.dnac._exec( + family="sda", + function='provision_wired_device', + op_modifies=True, + params=provision_wired_params, + ) + + if response.get("status") == "failed": + description = response.get("description") + error_msg = "Cannot do Provisioning for device {0} beacuse of {1}".format(device_ip, description) + self.log(error_msg) + continue + task_id = response.get("taskId") + + while True: + execution_details = self.get_task_details(task_id) + self.log(execution_details.get("progress")) + + if 'TASK_PROVISION' in execution_details.get("progress"): + self.result['changed'] = True + self.result['response'] = execution_details + provision_count += 1 + break + elif execution_details.get("isError") and execution_details.get("failureReason"): + self.msg = "Device Provisioning get failed because of {0}".format(execution_details.get("failureReason")) + self.status = "failed" + break + + except Exception as e: + error_message = "Error while Provisioning the device {0} in Cisco DNA Center - {1}".format(device_ip, str(e)) + self.log(error_message) + + if provision_count == len(device_ips): + self.status = "success" + msg = "Wired Device get provisioned Successfully !!" + elif provision_count == 0: + self.status = "failed" + msg = "Wired Device Provisioning get failed" + else: + self.status = "success" + msg = "Wired Device get provisioned Successfully Partially for {0} devices!!".format(provision_count) + self.log(msg) + + return self + def get_udf_id(self, field_name): """ Get the ID of a Global User Defined Field in Cisco DNA Center based on its name. - Args: + Paramters: self (object): An instance of a class used for interacting with Cisco Cisco DNA Center. field_name (str): The name of the Global User Defined Field. Returns: @@ -723,7 +851,7 @@ def get_udf_id(self, field_name): def mandatory_parameter(self): """ Check for and validate mandatory parameters for adding network devices in Cisco DNA Center. - Args: + Paramters: self (object): An instance of a class used for interacting with Cisco Cisco DNA Center. Returns: dict: The input `config` dictionary if all mandatory parameters are present. @@ -760,7 +888,7 @@ def mandatory_parameter(self): def get_have(self, config): """ Retrieve and check device information with Cisco DNA Center to determine if devices already exist. - Args: + Paramters: self (object): An instance of a class used for interacting with Cisco Cisco DNA Center. config (dict): A dictionary containing the configuration details of devices to be checked. Returns: @@ -796,7 +924,7 @@ def get_have(self, config): def get_device_params(self, params): """ Extract and store device parameters from the playbook for device processing in Cisco DNA Center. - Args: + Paramters: self (object): An instance of a class used for interacting with Cisco DNA Center. params (dict): A dictionary containing device parameters retrieved from the playbook. Returns: @@ -852,7 +980,7 @@ def get_device_params(self, params): def get_device_ids(self, device_ips): """ Get the list of unique device IDs for list of specified management IP addresses of devices in Cisco DNA Center. - Args: + Paramters: self (object): An instance of a class used for interacting with Cisco DNA Center. device_ips (list): The management IP addresses of devices for which you want to retrieve the device IDs. Returns: @@ -889,7 +1017,7 @@ def get_device_ids(self, device_ips): def get_interface_from_ip(self, device_ip): """ Get the interface ID for a device in Cisco DNA Center based on its IP address. - Args: + Paramters: self (object): An instance of a class used for interacting with Cisco DNA Center. device_ip (str): The IP address of the device. Returns: @@ -923,7 +1051,7 @@ def get_want(self, config): """ Get all the device related information from playbook that is needed to be add/update/delete/resync device in Cisco DNA Center. - Args: + Paramters: self (object): An instance of a class used for interacting with Cisco DNA Center. config (dict): A dictionary containing device-related information from the playbook. Returns: @@ -946,7 +1074,7 @@ def get_want(self, config): def get_diff_merged(self, config): """ Merge and process differences between existing devices and desired device configuration in Cisco DNA Center. - Args: + Paramters: self (object): An instance of a class used for interacting with Cisco DNA Center. config (dict): A dictionary containing the desired device configuration and relevant information from the playbook. Returns: @@ -961,10 +1089,162 @@ def get_diff_merged(self, config): device_type = self.config[0].get("type", "NETWORK_DEVICE") device_resynced = self.config[0].get("device_resync", False) device_updated = self.config[0].get("device_updated", False) + device_reboot = self.config[0].get("reboot_device", False) self.result['log'] = [] + if device_reboot: + device_ips = self.config[0].get("ip_address") + ap_mac_address_list = [] + # get and store the apEthernetMacAddress of given devices + for device_ip in device_ips: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"managementIpAddress": device_ip} + ) + response = response.get('response')[0] + if response['apEthernetMacAddress'] is not None: + ap_mac_address_list.append(response['apEthernetMacAddress']) + + if len(ap_mac_address_list) == 0: + self.status = "failed" + self.result['changed'] = False + msg = "Cannot find the AP devices for Rebooting" + self.log(msg) + self.msg = msg + return self + + # Now call the Reboot Access Point API + reboot_params = { + "apMacAddresses": ap_mac_address_list + } + response = self.dnac._exec( + family="wireless", + function='reboot_access_points', + op_modifies=True, + params=reboot_params, + ) + self.log(str(response)) + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + while True: + execution_details = self.get_task_details(task_id) + if 'url' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + break + elif execution_details.get("isError") and execution_details.get("failureReason"): + self.msg = "AP Device Reboot get failed because of {0}".format(execution_details.get("failureReason")) + self.status = "failed" + break + self.log("AP Devices Rebooted Successfully and Rebooted devices are :" + str(device_ips)) + msg = "Device " + str(device_ips) + " Rebooted Successfully !!" + self.result['log'].append(msg) + + return self + + if self.config[0].get('export_device_list'): + + device_ips = self.config[0].get("ip_address") + device_uuids = [] + try: + for device_ip in device_ips: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"managementIpAddress": device_ip} + ) + response = response.get('response') + if len(response) > 0: + device_uuids.append(response[0]["id"]) + + # Now all device UUID get collected so call the export device list API + + export_device_details = self.config[0].get('export_device_list') + payload_params = { + "deviceUuids": device_uuids, + "password": export_device_details.get("password"), + "operationEnum": export_device_details.get("operation_enum", "1"), + "paramters": export_device_details.get("paramters") + } + response = self.dnac._exec( + family="devices", + function='export_device_list', + op_modifies=True, + params=payload_params, + ) + self.log(str(response)) + response = response.get("response") + task_id = response.get("taskId") + # With this task ID call the Get Task Details API + task_resp = self.dnac._exec( + family="task", + function='get_task_by_id', + op_modifies=True, + params={"task_id": task_id}, + ) + + while True: + execution_details = self.get_task_details(task_id) + + if execution_details.get("additionalStatusURL"): + file_id = execution_details.get("additionalStatusURL").split("/")[-1] + break + elif execution_details.get("isError") and execution_details.get("failureReason"): + failed_reason = execution_details.get("failureReason") + msg = "Could not get the File ID because of {0} so can't export device details in csv file".format(failed_reason) + self.msg = msg + self.log(msg) + self.status = "failed" + return self + + # With this File ID call the Download File by FileID API + response = self.dnac._exec( + family="file", + function='download_a_file_by_fileid', + op_modifies=True, + params={"file_id": file_id}, + ) + + device_data = [] + encoded_resp = response.data.decode(encoding='utf-8') + self.log(str(encoded_resp)) + + # Parse the CSV-like string into a list of dictionaries + csv_reader = csv.DictReader(StringIO(encoded_resp)) + for row in csv_reader: + device_data.append(row) + + current_date = datetime.now() + formatted_date = current_date.strftime("%m-%d-%Y") + file_name = "devices-" + str(formatted_date) + ".csv" + + # Write the data to a CSV file + with open(file_name, 'w', newline='') as csv_file: + fieldnames = device_data[0].keys() + csv_writer = csv.DictWriter(csv_file, fieldnames=fieldnames) + csv_writer.writeheader() + csv_writer.writerows(device_data) + + msg = "Device Details Exported Successfully to the CSV file - {0}".format(file_name) + self.log(msg) + self.status = "success" + self.result['changed'] = True + self.result['log'].append(msg) + + except Exception as e: + msg = "Cannot Export the Device Details into CSV file for {0}".format(str(device_ips)) + self.log(msg) + self.status = "failed" + self.msg = msg + + return self + if self.config[0].get('add_user_defined_field'): field_name = self.config[0].get('add_user_defined_field').get('name') + if field_name is None: self.msg = "Mandatory paramter for User Define Field - name is missing" self.status = "failed" @@ -977,8 +1257,9 @@ def get_diff_merged(self, config): self.create_user_defined_field().check_return_status() # Get device Id with its IP Address - device_ips = config.get("ip_address") + device_ips = self.config[0].get("ip_address") device_ids = self.get_device_ids(device_ips) + if len(device_ids) == 0: self.msg = "Can't Assign Global User Defined Field to device as device's are not present in Cisco DNA Center" self.status = "failed" @@ -999,6 +1280,7 @@ def get_diff_merged(self, config): # Code for triggers the resync operation using the retrieved device IDs and force sync parameter. device_ips = config.get("ip_address") device_ids = self.get_device_ids(device_ips) + if len(device_ids) == 0: self.msg = "Cannot perform the Resync operation as device's are not present in Cisco DNA Center" self.status = "failed" @@ -1021,8 +1303,10 @@ def get_diff_merged(self, config): if response and isinstance(response, dict): task_id = response.get('response').get('taskId') + while True: execution_details = self.get_task_details(task_id) + if 'Synced' in execution_details.get("progress"): self.status = "success" self.result['changed'] = True @@ -1068,6 +1352,7 @@ def get_diff_merged(self, config): for device_ip in device_to_update: device_id = self.get_device_ids([device_ip]) device_role_args = self.config[0].get('update_device_role') + if 'role' not in device_role_args or 'role_source' not in device_role_args: self.msg = "Mandatory paramter(role/sourceRole) to update Device Role are missing" self.status = "failed" @@ -1080,6 +1365,7 @@ def get_diff_merged(self, config): params={"managementIpAddress": device_ip} ) response = response.get('response')[0] + if response.get('role') == device_role_args.get('role'): self.status = "success" self.result['changed'] = False @@ -1104,6 +1390,7 @@ def get_diff_merged(self, config): if response and isinstance(response, dict): task_id = response.get('response').get('taskId') + while True: execution_details = self.get_task_details(task_id) @@ -1160,6 +1447,7 @@ def get_diff_merged(self, config): if response and isinstance(response, dict): task_id = response.get('response').get('taskId') + while True: execution_details = self.get_task_details(task_id) @@ -1199,6 +1487,7 @@ def get_diff_merged(self, config): if response and isinstance(response, dict): task_id = response.get('response').get('taskId') + while True: execution_details = self.get_task_details(task_id) @@ -1243,12 +1532,14 @@ def get_diff_merged(self, config): if response and isinstance(response, dict): task_id = response.get('response').get('taskId') + while True: execution_details = self.get_task_details(task_id) if '/task/' in execution_details.get("progress"): self.status = "success" self.result['response'] = execution_details + if len(devices_to_add) > 0: self.result['changed'] = True log("Device Added Successfully") @@ -1269,22 +1560,28 @@ def get_diff_merged(self, config): self.log(error_message) raise Exception(error_message) + # Once device get added we will assign device to site and Provisioned it + if self.config[0].get('provision_wired_device'): + self.provisioned_wired_device() + return self def get_diff_deleted(self, config): """ Delete devices in Cisco DNA Center based on device IP Address. - Args: + Paramters: self (object): An instance of a class used for interacting with Cisco DNA Center config (dict): A dictionary containing the list of device IP addresses to be deleted. Returns: object: An instance of the class with updated results and status based on the deletion operation. Description: This function is responsible for removing devices from the Cisco DNA Center inventory and - raise Exception if any error occured. + also unprovsioned and removed wired provsion devices from the Inventory page and also delete + the Global User Defined Field that are associated to the devices. """ device_to_delete = config.get("ip_address") + provision_device = self.config[0].get('delete_provision_device', 'False') self.result['msg'] = [] if self.config[0].get('add_user_defined_field'): @@ -1294,7 +1591,6 @@ def get_diff_deleted(self, config): if udf_id is None: msg = "Global UDF - {0} is not present in Cisco DNA Center".format(field_name) self.msg = msg - self.result['msg'] self.status = "success" self.result['changed'] = False self.result['msg'].append(msg) @@ -1308,8 +1604,10 @@ def get_diff_deleted(self, config): ) if response and isinstance(response, dict): task_id = response.get('response').get('taskId') + while True: execution_details = self.get_task_details(task_id) + if 'success' in execution_details.get("progress"): self.msg = "Global UDF - {0} Deleted Successfully from Cisco DNA Center".format(field_name) self.status = "success" @@ -1333,16 +1631,42 @@ def get_diff_deleted(self, config): msg = "The device {0} is not present in Cisco DNA Center so can't perform delete operation".format(device_ip) self.msg = msg self.status = "success" + self.result['changed'] = False self.result['msg'].append(msg) continue - device_id = self.get_device_ids([device_ip]) - delete_params = { - "id": device_id[0], - "clean_config": self.config[0].get("clean_config", False) - } - try: + provision_params = { + "device_management_ip_address": device_ip + } + prov_respone = self.dnac._exec( + family="sda", + function='get_provisioned_wired_device', + params=provision_params, + ) + + if prov_respone.get("status") == "success": + response = self.dnac._exec( + family="sda", + function='delete_provisioned_wired_device', + params=provision_params, + ) + if response.get("status") == "success": + msg = "Wired device {0} unprovisioned successfully.".format(device_ip) + self.log(msg) + self.result['changed'] = True + self.status = "success" + else: + msg = response.get("description") + self.log(msg) + self.status = "failed" + + except Exception as e: + device_id = self.get_device_ids([device_ip]) + delete_params = { + "id": device_id[0], + "clean_config": self.config[0].get("clean_config", False) + } response = self.dnac._exec( family="devices", function='delete_device_by_id', @@ -1351,6 +1675,7 @@ def get_diff_deleted(self, config): if response and isinstance(response, dict): task_id = response.get('response').get('taskId') + while True: execution_details = self.get_task_details(task_id) @@ -1365,11 +1690,6 @@ def get_diff_deleted(self, config): self.status = "failed" break - except Exception as e: - error_message = "Error while Deleting device from Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) - raise Exception(error_message) - return self From 302e416af1da272cc22cae6bc84d18ca47dc6587 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 12 Dec 2023 15:29:02 +0530 Subject: [PATCH 33/50] Addressed the PR comments --- plugins/modules/template_intent.py | 66 +++++++++++++++++++----------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index 4335955a01..3dcc3f7f96 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -3,6 +3,7 @@ # Copyright (c) 2022, Cisco Systems # GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) """Ansible module to perform operations on project and templates in DNAC.""" + from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -1288,7 +1289,9 @@ """ +from math import exp import copy +import json from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( DnacBase, @@ -1435,6 +1438,8 @@ def validate_input(self): } } # Validate template params + self.log(str(self.config)) + self.log(str(temp_spec)) valid_temp, invalid_params = validate_list_of_dicts( self.config, temp_spec ) @@ -1594,7 +1599,9 @@ def get_template_info(self, template_params): templateParams = [] i = 0 + self.log(str(template_params)) for item in template_params: + self.log(str(item)) templateParams.append({}) binding = item.get("binding") if binding is not None: @@ -1682,26 +1689,28 @@ def get_template_info(self, template_params): j = 0 for value in range: _range.append({}) - id = range[j].get("id") + id = value.get("id") if id is not None: - _range.update({"id": id}) - max_value = range[j].get("max_value") + _range[j].update({"id": id}) + max_value = value.get("max_value") if max_value is not None: - _range.update({"maxValue": max_value}) + _range[j].update({"maxValue": max_value}) else: self.msg = "max_value is mandatory for range under template_params" self.status = "failed" return self.check_return_status() - min_value = range[j].get("min_value") + min_value = value.get("min_value") if min_value is not None: - _range.update({"maxValue": min_value}) + _range[j].update({"maxValue": min_value}) else: self.msg = "min_value is mandatory for range under template_params" self.status = "failed" return self.check_return_status() j = j + 1 + self.log(str(templateParams)) selection = item.get("selection") + self.log(str(selection)) if selection is not None: templateParams[i].update({"selection": {}}) _selection = templateParams[i].get("selection") @@ -1711,25 +1720,12 @@ def get_template_info(self, template_params): default_selected_values = selection.get("default_selected_values") if default_selected_values is not None: _selection.update({"defaultSelectedValues": default_selected_values}) - selection_type = selection.get("selection_type") - selectiontypes = ["SINGLE_SELECT", "MULTI_SELECT"] - if selection_type is not None: - _selection.update({"selectionType": selection_type}) - else: - self.msg = "selection_type is mandatory in selection." - self.status = "failed" - return self.check_return_status() - if selection_type not in selectiontypes: - self.msg = "selection_type under selection must be in " + str(selectiontypes) - self.status = "failed" - return self.check_return_status() selection_values = selection.get("selection_values") if selection_values is not None: _selection.update({"selectionValues": selection_values}) - else: - self.msg = "selection_values is mandatory in selection" - self.status = "failed" - return self.check_return_status() + selection_type = selection.get("selection_type") + if selection_type is not None: + _selection.update({"selectionType": selection_type}) i = i + 1 return templateParams @@ -1838,6 +1834,7 @@ def get_template_params(self, params): temp_params (dict) - Organized template parameters. """ + self.log(str(params)) temp_params = { "tags": self.get_tags(params.get("template_tag")), "author": params.get("author"), @@ -2063,6 +2060,7 @@ def get_want(self, config): want = {} configuration_templates = config.get("configuration_templates") + self.log(str(config)) if configuration_templates: template_params = self.get_template_params(configuration_templates) project_params = self.get_project_params(configuration_templates) @@ -2081,6 +2079,23 @@ def get_want(self, config): self.status = "success" return self + def check_string_dictionary(self, task_details_data): + """ + Check whether the input is string dictionary or string. + + Parameters: + task_details_data (string) - Input either string dictionary or string. + + Returns: + value (dict) - If the input is string dictionary, else returns None. + """ + + try: + value = json.loads(task_details_data) + return value + except json.JSONDecodeError: + return None + def create_project_or_template(self, is_create_project=False): """ Call DNAC API to create project or template based on the input provided. @@ -2142,7 +2157,12 @@ def create_project_or_template(self, is_create_project=False): "for taskid: {1}".format(task_details.get('progress'), task_id)) continue - creation_id = task_details.get("data") + task_details_data = task_details.get("data") + value = self.check_string_dictionary(task_details_data) + if value is None: + creation_id = task_details.get("data") + else: + creation_id = value.get("data").get("templateId") if not creation_id: self.log("data is not found for taskid: {0}".format(task_id)) continue From a8ef6485f7324e9897baa677c89badbc22d0c0e5 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 12 Dec 2023 15:35:02 +0530 Subject: [PATCH 34/50] Resolved a bug --- plugins/modules/template_intent.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index 3dcc3f7f96..13f0dc69b2 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -1289,7 +1289,6 @@ """ -from math import exp import copy import json from ansible.module_utils.basic import AnsibleModule From bf2208886d5514326d0adb53504bd4f6095bad8d Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 13 Dec 2023 10:15:40 +0530 Subject: [PATCH 35/50] Moved the check_string_dictionary function to dnac.py --- plugins/module_utils/dnac.py | 18 ++++++++++++++++++ plugins/modules/template_intent.py | 18 ------------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index b8a42828d2..58f15a737d 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -23,6 +23,7 @@ LOGGING_IN_STANDARD = True import os.path import copy +import json import datetime import inspect @@ -276,6 +277,23 @@ def check_execution_response_status(self, response): return self + def check_string_dictionary(self, task_details_data): + """ + Check whether the input is string dictionary or string. + + Parameters: + task_details_data (string) - Input either string dictionary or string. + + Returns: + value (dict) - If the input is string dictionary, else returns None. + """ + + try: + value = json.loads(task_details_data) + return value + except json.JSONDecodeError: + return None + def log(msg, frameIncrement=0): with open('dnac.log', 'a') as of: diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index 13f0dc69b2..8675417ea5 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -1290,7 +1290,6 @@ """ import copy -import json from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( DnacBase, @@ -2078,23 +2077,6 @@ def get_want(self, config): self.status = "success" return self - def check_string_dictionary(self, task_details_data): - """ - Check whether the input is string dictionary or string. - - Parameters: - task_details_data (string) - Input either string dictionary or string. - - Returns: - value (dict) - If the input is string dictionary, else returns None. - """ - - try: - value = json.loads(task_details_data) - return value - except json.JSONDecodeError: - return None - def create_project_or_template(self, is_create_project=False): """ Call DNAC API to create project or template based on the input provided. From 002ea9b292a7f7ea333d2fe3a593174b4db013fb Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 13 Dec 2023 14:53:17 +0530 Subject: [PATCH 36/50] validating the input in check string dictionary --- plugins/module_utils/dnac.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 58f15a737d..bce97cd5aa 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -290,9 +290,11 @@ def check_string_dictionary(self, task_details_data): try: value = json.loads(task_details_data) - return value + if isinstance(value, dict): + return value except json.JSONDecodeError: - return None + pass + return None def log(msg, frameIncrement=0): From 2d916b4749c49bf36ff884a8cb6e1bf8be458459 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 13 Dec 2023 17:02:23 +0530 Subject: [PATCH 37/50] Add seperate API for Reboot AP, export device list into csv and code optimisation --- plugins/modules/inventory_intent.py | 500 +++++++++++++++++----------- 1 file changed, 305 insertions(+), 195 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 45dc1899d1..720a9eb3f1 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -259,7 +259,6 @@ - exist_mgmt_ipaddress: string new_mgmt_ipaddress: string username: string - device_resync: false - name: Add new Compute device in Inventory with full credentials.Inputs needed for Compute Device cisco.dnac.inventory_intent: @@ -286,7 +285,6 @@ snmp_timeout: 5 snmp_username: string username: string - device_resync: false type: "COMPUTE_DEVICE" - name: Add new Meraki device in Inventory with full credentials.Inputs needed for Meraki Device. @@ -302,7 +300,6 @@ state: merged config: - http_password: string - device_resync: false type: "MERAKI_DASHBOARD" - name: Add new Firepower Management device in Inventory with full credentials.Input needed to add Device. @@ -321,7 +318,6 @@ http_username: string http_password: string http_port: string - device_resync: false type: "FIREPOWER_MANAGEMENT_SYSTEM" - name: Add new Third Party device in Inventory with full credentials.Input needed to add Device. @@ -345,7 +341,6 @@ snmp_retry: 3 snmp_timeout: 5 snmp_username: string - device_resync: false type: "THIRD_PARTY_DEVICE" - name: Associate Wired Devices to site and Provisioned it in Inventory @@ -362,7 +357,7 @@ config: - ip_address: string provision_wired_device: - site_name: "{{item.site_name}}" + site_name: string - name: Update Device Role with IP Address cisco.dnac.inventory_intent: @@ -469,7 +464,7 @@ - ip_address: string reboot_device: True -- name: Delete Device by id +- name: Delete Provision/Unprovisioned Devices by IP Address cisco.dnac.inventory_intent: dnac_host: "{{dnac_host}}" dnac_username: "{{dnac_username}}" @@ -483,7 +478,6 @@ config: - ip_address: string clean_config: false - id: string - name: Delete Global User Defined Field with name cisco.dnac.inventory_intent: @@ -542,7 +536,7 @@ def validate_input(self): Validate the fields provided in the playbook. Checks the configuration provided in the playbook against a predefined specification to ensure it adheres to the expected structure and data types. - Paramters: + 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: @@ -614,7 +608,7 @@ def validate_input(self): def device_exists_in_dnac(self): """ Check which devices already exists in Cisco DNA Center and return both device_exist and device_not_exist in dnac. - Paramters: + Parameters: self (object): An instance of a class used for interacting with Cisco Cisco DNA Center. Returns: list: A list of devices that exist in Cisco DNA Center. @@ -651,7 +645,7 @@ def device_exists_in_dnac(self): def is_udf_exist(self, field_name): """ Check if a Global User Defined Field exists in Cisco DNA Center based on its name. - Paramters: + Parameters: self (object): An instance of a class used for interacting with Cisco DNA Center. field_name (str): The name of the Global User Defined Field. Returns: @@ -681,7 +675,7 @@ def is_udf_exist(self, field_name): def create_user_defined_field(self): """ Create a Global User Defined Field in Cisco DNA Center based on the provided configuration. - Paramters: + Parameters: self (object): An instance of a class used for interacting with Cisco DNA Center. Returns: self (object): An instance of a class used for interacting with Cisco DNA Center. @@ -711,7 +705,7 @@ def create_user_defined_field(self): def add_field_to_devices(self, device_ids): """ Add a Global user-defined field with specified details to a list of devices in Cisco DNA Center. - Paramters: + Parameters: self (object): An instance of a class used for interacting with Cisco DNA Center. device_ids (list): A list of device IDs to which the user-defined field will be added. Returns: @@ -751,10 +745,209 @@ def add_field_to_devices(self, device_ids): return self + def export_device_details(self): + """ + Export device details from Cisco DNA Center into a CSV file. + Parameters: + self (object): An instance of a class used for interacting with Cisco DNA Center. + Returns: + self (object): An instance of the class with updated result, status, and log. + Description: + This function exports device details from Cisco DNA Center based on the provided IP addresses in the configuration. + It retrieves the device UUIDs, calls the export device list API, and downloads the exported data in CSV format. + The CSV data is then parsed and written to a file. + """ + + device_ips = self.config[0].get("ip_address", []) + + if not(device_ips): + msg = "No Devices are given in the playbook so can't export device details" + self.status = "failed" + self.msg = msg + self.log(msg) + return self + + device_uuids = [] + try: + for device_ip in device_ips: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"managementIpAddress": device_ip} + ) + response = response.get('response') + if response: + device_uuids.append(response[0]["id"]) + + if not device_uuids: + self.status = "failed" + self.result['changed'] = False + self.msg = "Could not find device UUIDs for exporting device details" + self.log(self.msg) + return self + + # Now all device UUID get collected so call the export device list API + export_device_list = self.config[0].get('export_device_list') + payload_params = { + "deviceUuids": device_uuids, + "password": export_device_list.get("password"), + "operationEnum": export_device_list.get("operation_enum", "1"), + "paramters": export_device_list.get("paramters") + } + + response = self.dnac._exec( + family="devices", + function='export_device_list', + op_modifies=True, + params=payload_params, + ) + self.log(str(response)) + response = response.get("response") + task_id = response.get("taskId") + + while True: + execution_details = self.get_task_details(task_id) + + if execution_details.get("additionalStatusURL"): + file_id = execution_details.get("additionalStatusURL").split("/")[-1] + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Could not get the File ID because of {0} so can't export device details in csv file".format(failure_reason) + else: + self.msg = "Could not get the File ID so can't export device details in csv file" + self.log(self.msg) + + return self + + # With this File ID call the Download File by FileID API + response = self.dnac._exec( + family="file", + function='download_a_file_by_fileid', + op_modifies=True, + params={"file_id": file_id}, + ) + + device_data = [] + encoded_resp = response.data.decode(encoding='utf-8') + self.log(str(encoded_resp)) + + # Parse the CSV-like string into a list of dictionaries + csv_reader = csv.DictReader(StringIO(encoded_resp)) + for row in csv_reader: + device_data.append(row) + + current_date = datetime.now() + formatted_date = current_date.strftime("%m-%d-%Y") + file_name = "devices-" + str(formatted_date) + ".csv" + + # Write the data to a CSV file + with open(file_name, 'w', newline='') as csv_file: + fieldnames = device_data[0].keys() + csv_writer = csv.DictWriter(csv_file, fieldnames=fieldnames) + csv_writer.writeheader() + csv_writer.writerows(device_data) + + msg = "Device Details Exported Successfully to the CSV file - {0}".format(file_name) + self.log(msg) + self.status = "success" + self.result['changed'] = True + self.result['log'].append(msg) + + except Exception as e: + self.msg = "Cannot Export the Device Details into CSV file for {0}".format(str(device_ips)) + self.log(self.msg) + self.status = "failed" + + return self + + def reboot_access_points(self): + """ + Reboot access points in Cisco DNA Center. + Parameters: + self (object): An instance of a class used for interacting with Cisco DNA Center. + Returns: + self (object): An instance of the class with updated result, status, and log. + Description: + This function performs a reboot operation on access points in Cisco DNA Center based on the provided IP addresses + in the configuration. It retrieves the AP devices' MAC addresses, calls the reboot access points API, and monitors + the progress of the reboot operation. + """ + + device_ips = self.config[0].get("ip_address", []) + + if not(device_ips): + msg = "No AP Devices IP given in the playbook so can't perform reboot operation" + self.status = "failed" + self.msg = msg + self.log(msg) + return self + + ap_mac_address_list = [] + # Get and store the apEthernetMacAddress of given devices + for device_ip in device_ips: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"managementIpAddress": device_ip} + ) + response = response.get('response')[0] + ap_mac_address = response.get('apEthernetMacAddress') + + if ap_mac_address is not None: + ap_mac_address_list.append(ap_mac_address) + + if not ap_mac_address_list: + self.status = "failed" + self.result['changed'] = False + self.msg = "Cannot find the AP devices for rebooting" + self.log(self.msg) + return self + + # Now call the Reboot Access Point API + reboot_params = { + "apMacAddresses": ap_mac_address_list + } + response = self.dnac._exec( + family="wireless", + function='reboot_access_points', + op_modifies=True, + params=reboot_params, + ) + self.log(str(response)) + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + + if 'url' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "AP Device Rebooting get failed because of {0}".format(failure_reason) + else: + self.msg = "AP Device Rebooting get failed" + break + + self.log("AP Devices Rebooted Successfully and Rebooted devices are :" + str(device_ips)) + msg = "Device " + str(device_ips) + " Rebooted Successfully !!" + self.result['log'].append(msg) + + return self + def provisioned_wired_device(self): """ Provision wired devices in Cisco DNA Center. - Paramters: + Parameters: self (object): An instance of a class used for interacting with Cisco DNA Center. Returns: self (object): An instance of the class with updated result, status, and log. @@ -764,16 +957,17 @@ def provisioned_wired_device(self): attempts to provision each device, and monitors the provisioning process. """ - site_name = self.config[0].get('provision_wired_device').get("site_name") - device_ips = self.config[0].get("ip_address") - provision_count = 0 + site_name = self.config[0]['provision_wired_device']['site_name'] + device_ips = self.config[0]['ip_address'] + provision_count, already_provision_count = 0, 0 + provision_wired_params = { + 'siteNameHierarchy': site_name + } for device_ip in device_ips: try: - provision_wired_params = { - 'deviceManagementIpAddress': device_ip, - 'siteNameHierarchy': site_name, - } + provision_wired_params['deviceManagementIpAddress'] = device_ip + response = self.dnac._exec( family="sda", function='provision_wired_device', @@ -786,26 +980,47 @@ def provisioned_wired_device(self): error_msg = "Cannot do Provisioning for device {0} beacuse of {1}".format(device_ip, description) self.log(error_msg) continue + task_id = response.get("taskId") while True: execution_details = self.get_task_details(task_id) - self.log(execution_details.get("progress")) + progress = execution_details.get("progress") + self.log(progress) - if 'TASK_PROVISION' in execution_details.get("progress"): + if 'TASK_PROVISION' in progress: self.result['changed'] = True self.result['response'] = execution_details provision_count += 1 break - elif execution_details.get("isError") and execution_details.get("failureReason"): - self.msg = "Device Provisioning get failed because of {0}".format(execution_details.get("failureReason")) + elif execution_details.get("isError"): self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device Provisioning get failed because of {0}".format(failure_reason) + else: + self.msg = "Device Provisioning get failed" break except Exception as e: + # Not returning from here as there might be possiblity that for some devices it comes into exception + # but for others it gets provision successfully or If some devices are already provsioned error_message = "Error while Provisioning the device {0} in Cisco DNA Center - {1}".format(device_ip, str(e)) self.log(error_message) + if "already provisioned" in str(e): + self.log(str(e)) + already_provision_count += 1 + + # Check If all the devices are already provsioned, return from here only + if already_provision_count == len(device_ips): + self.status = "success" + self.msg = "All the Wired Devices given in the playbook are already Provisioned" + self.log(self.msg) + self.result['response'] = self.msg + self.result['changed'] = False + return self + if provision_count == len(device_ips): self.status = "success" msg = "Wired Device get provisioned Successfully !!" @@ -822,7 +1037,7 @@ def provisioned_wired_device(self): def get_udf_id(self, field_name): """ Get the ID of a Global User Defined Field in Cisco DNA Center based on its name. - Paramters: + Parameters: self (object): An instance of a class used for interacting with Cisco Cisco DNA Center. field_name (str): The name of the Global User Defined Field. Returns: @@ -832,6 +1047,7 @@ def get_udf_id(self, field_name): with the specified name and extracts the ID of the first matching field.If successful, it returns the ID else returns None. """ + try: response = self.dnac._exec( family="devices", @@ -851,7 +1067,7 @@ def get_udf_id(self, field_name): def mandatory_parameter(self): """ Check for and validate mandatory parameters for adding network devices in Cisco DNA Center. - Paramters: + Parameters: self (object): An instance of a class used for interacting with Cisco Cisco DNA Center. Returns: dict: The input `config` dictionary if all mandatory parameters are present. @@ -888,7 +1104,7 @@ def mandatory_parameter(self): def get_have(self, config): """ Retrieve and check device information with Cisco DNA Center to determine if devices already exist. - Paramters: + Parameters: self (object): An instance of a class used for interacting with Cisco Cisco DNA Center. config (dict): A dictionary containing the configuration details of devices to be checked. Returns: @@ -924,7 +1140,7 @@ def get_have(self, config): def get_device_params(self, params): """ Extract and store device parameters from the playbook for device processing in Cisco DNA Center. - Paramters: + Parameters: self (object): An instance of a class used for interacting with Cisco DNA Center. params (dict): A dictionary containing device parameters retrieved from the playbook. Returns: @@ -980,7 +1196,7 @@ def get_device_params(self, params): def get_device_ids(self, device_ips): """ Get the list of unique device IDs for list of specified management IP addresses of devices in Cisco DNA Center. - Paramters: + Parameters: self (object): An instance of a class used for interacting with Cisco DNA Center. device_ips (list): The management IP addresses of devices for which you want to retrieve the device IDs. Returns: @@ -991,6 +1207,7 @@ def get_device_ids(self, device_ips): """ device_ids = [] + for device_ip in device_ips: try: response = self.dnac._exec( @@ -1002,7 +1219,7 @@ def get_device_ids(self, device_ips): if response: self.log(str(response)) response = response.get("response") - if len(response) == 0: + if not response: continue device_id = response[0]["id"] device_ids.append(device_id) @@ -1017,7 +1234,7 @@ def get_device_ids(self, device_ips): def get_interface_from_ip(self, device_ip): """ Get the interface ID for a device in Cisco DNA Center based on its IP address. - Paramters: + Parameters: self (object): An instance of a class used for interacting with Cisco DNA Center. device_ip (str): The IP address of the device. Returns: @@ -1037,7 +1254,7 @@ def get_interface_from_ip(self, device_ip): self.log(str(response)) response = response.get("response") - if len(response) > 0: + if response: interface_id = response[0]["id"] self.log("Fetch Interface Id for device {0} successfully !!".format(device_ip)) return interface_id @@ -1051,7 +1268,7 @@ def get_want(self, config): """ Get all the device related information from playbook that is needed to be add/update/delete/resync device in Cisco DNA Center. - Paramters: + Parameters: self (object): An instance of a class used for interacting with Cisco DNA Center. config (dict): A dictionary containing device-related information from the playbook. Returns: @@ -1074,7 +1291,7 @@ def get_want(self, config): def get_diff_merged(self, config): """ Merge and process differences between existing devices and desired device configuration in Cisco DNA Center. - Paramters: + Parameters: self (object): An instance of a class used for interacting with Cisco DNA Center. config (dict): A dictionary containing the desired device configuration and relevant information from the playbook. Returns: @@ -1093,153 +1310,11 @@ def get_diff_merged(self, config): self.result['log'] = [] if device_reboot: - device_ips = self.config[0].get("ip_address") - ap_mac_address_list = [] - # get and store the apEthernetMacAddress of given devices - for device_ip in device_ips: - response = self.dnac._exec( - family="devices", - function='get_device_list', - params={"managementIpAddress": device_ip} - ) - response = response.get('response')[0] - if response['apEthernetMacAddress'] is not None: - ap_mac_address_list.append(response['apEthernetMacAddress']) - - if len(ap_mac_address_list) == 0: - self.status = "failed" - self.result['changed'] = False - msg = "Cannot find the AP devices for Rebooting" - self.log(msg) - self.msg = msg - return self - - # Now call the Reboot Access Point API - reboot_params = { - "apMacAddresses": ap_mac_address_list - } - response = self.dnac._exec( - family="wireless", - function='reboot_access_points', - op_modifies=True, - params=reboot_params, - ) - self.log(str(response)) - - if response and isinstance(response, dict): - task_id = response.get('response').get('taskId') - while True: - execution_details = self.get_task_details(task_id) - if 'url' in execution_details.get("progress"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - break - elif execution_details.get("isError") and execution_details.get("failureReason"): - self.msg = "AP Device Reboot get failed because of {0}".format(execution_details.get("failureReason")) - self.status = "failed" - break - self.log("AP Devices Rebooted Successfully and Rebooted devices are :" + str(device_ips)) - msg = "Device " + str(device_ips) + " Rebooted Successfully !!" - self.result['log'].append(msg) - + self.reboot_access_points() return self if self.config[0].get('export_device_list'): - - device_ips = self.config[0].get("ip_address") - device_uuids = [] - try: - for device_ip in device_ips: - response = self.dnac._exec( - family="devices", - function='get_device_list', - params={"managementIpAddress": device_ip} - ) - response = response.get('response') - if len(response) > 0: - device_uuids.append(response[0]["id"]) - - # Now all device UUID get collected so call the export device list API - - export_device_details = self.config[0].get('export_device_list') - payload_params = { - "deviceUuids": device_uuids, - "password": export_device_details.get("password"), - "operationEnum": export_device_details.get("operation_enum", "1"), - "paramters": export_device_details.get("paramters") - } - response = self.dnac._exec( - family="devices", - function='export_device_list', - op_modifies=True, - params=payload_params, - ) - self.log(str(response)) - response = response.get("response") - task_id = response.get("taskId") - # With this task ID call the Get Task Details API - task_resp = self.dnac._exec( - family="task", - function='get_task_by_id', - op_modifies=True, - params={"task_id": task_id}, - ) - - while True: - execution_details = self.get_task_details(task_id) - - if execution_details.get("additionalStatusURL"): - file_id = execution_details.get("additionalStatusURL").split("/")[-1] - break - elif execution_details.get("isError") and execution_details.get("failureReason"): - failed_reason = execution_details.get("failureReason") - msg = "Could not get the File ID because of {0} so can't export device details in csv file".format(failed_reason) - self.msg = msg - self.log(msg) - self.status = "failed" - return self - - # With this File ID call the Download File by FileID API - response = self.dnac._exec( - family="file", - function='download_a_file_by_fileid', - op_modifies=True, - params={"file_id": file_id}, - ) - - device_data = [] - encoded_resp = response.data.decode(encoding='utf-8') - self.log(str(encoded_resp)) - - # Parse the CSV-like string into a list of dictionaries - csv_reader = csv.DictReader(StringIO(encoded_resp)) - for row in csv_reader: - device_data.append(row) - - current_date = datetime.now() - formatted_date = current_date.strftime("%m-%d-%Y") - file_name = "devices-" + str(formatted_date) + ".csv" - - # Write the data to a CSV file - with open(file_name, 'w', newline='') as csv_file: - fieldnames = device_data[0].keys() - csv_writer = csv.DictWriter(csv_file, fieldnames=fieldnames) - csv_writer.writeheader() - csv_writer.writerows(device_data) - - msg = "Device Details Exported Successfully to the CSV file - {0}".format(file_name) - self.log(msg) - self.status = "success" - self.result['changed'] = True - self.result['log'].append(msg) - - except Exception as e: - msg = "Cannot Export the Device Details into CSV file for {0}".format(str(device_ips)) - self.log(msg) - self.status = "failed" - self.msg = msg - + self.export_device_details() return self if self.config[0].get('add_user_defined_field'): @@ -1312,9 +1387,14 @@ def get_diff_merged(self, config): self.result['changed'] = True self.result['response'] = execution_details break - elif execution_details.get("isError") and execution_details.get("failureReason"): - self.msg = "Device Resynced get failed because of {0}".format(execution_details.get("failureReason")) + elif execution_details.get("isError"): self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device Resynced get failed because of {0}".format(failure_reason) + else: + self.msg = "Device Resynced get failed." + self.log(self.msg) break self.log("Device Resynced Successfully and Resynced devices are :" + str(device_ips)) msg = "Device " + str(device_ips) + " Resynced Successfully !!" @@ -1402,9 +1482,14 @@ def get_diff_merged(self, config): msg = "Device " + str(device_to_update) + " Role updated Successfully !!" self.result['log'].append(msg) break - elif execution_details.get("isError") and execution_details.get("failureReason"): - self.msg = "Device Role Updation get failed because of {0}".format(execution_details.get("failureReason")) + elif execution_details.get("isError"): self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device Role Updation get failed because of {0}".format(failure_reason) + else: + self.msg = "Device Role Updation get failed" + self.log(self.msg) break except Exception as e: @@ -1459,9 +1544,14 @@ def get_diff_merged(self, config): log(log_msg) self.result['log'].append(log_msg) break - elif execution_details.get("isError") and execution_details.get("failureReason"): - self.msg = "Interface Updation get failed because of {0}".format(execution_details.get("failureReason")) + elif execution_details.get("isError"): self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Interface Updation get failed because of {0}".format(failure_reason) + else: + self.msg = "Interface Updation get failed" + self.log(self.msg) break except Exception as e: @@ -1496,9 +1586,14 @@ def get_diff_merged(self, config): self.result['changed'] = True self.result['response'] = execution_details break - elif execution_details.get("isError") and execution_details.get("failureReason"): - self.msg = "Device Updation get failed because of {0}".format(execution_details.get("failureReason")) + elif execution_details.get("isError"): self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device Updation get failed because of {0}".format(failure_reason) + else: + self.msg = "Device Updation get failed" + self.log(self.msg) break log("Device Updated Successfully") @@ -1550,9 +1645,14 @@ def get_diff_merged(self, config): msg = "Devices " + str(self.config[0].get("ip_address")) + " already present in Cisco DNA Center" self.result['log'].append(msg) break - elif execution_details.get("isError") and execution_details.get("failureReason"): - self.msg = "Device Addition get failed because of {0}".format(execution_details.get("failureReason")) + elif execution_details.get("isError"): self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device Addition get failed because of {0}".format(failure_reason) + else: + self.msg = "Device Addition get failed" + self.log(self.msg) break except Exception as e: @@ -1569,7 +1669,7 @@ def get_diff_merged(self, config): def get_diff_deleted(self, config): """ Delete devices in Cisco DNA Center based on device IP Address. - Paramters: + Parameters: self (object): An instance of a class used for interacting with Cisco DNA Center config (dict): A dictionary containing the list of device IP addresses to be deleted. Returns: @@ -1581,7 +1681,6 @@ def get_diff_deleted(self, config): """ device_to_delete = config.get("ip_address") - provision_device = self.config[0].get('delete_provision_device', 'False') self.result['msg'] = [] if self.config[0].get('add_user_defined_field'): @@ -1614,10 +1713,16 @@ def get_diff_deleted(self, config): self.result['changed'] = True self.result['response'] = execution_details break - elif execution_details.get("isError") and execution_details.get("failureReason"): - self.msg = "Global UDF Deletion get failed because of {0}".format(execution_details.get("failureReason")) + elif execution_details.get("isError"): self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Global UDF Deletion get failed because of {0}".format(failure_reason) + else: + self.msg = "Global UDF Deletion get failed." + self.log(self.msg) break + except Exception as e: error_message = "Error while Deleting Global UDF from Cisco DNA Center - {0}".format(str(e)) self.log(error_message) @@ -1685,9 +1790,14 @@ def get_diff_deleted(self, config): self.result['changed'] = True self.result['response'] = execution_details break - elif execution_details.get("isError") and execution_details.get("failureReason"): - self.msg = "Device Deletion get failed because of {0}".format(execution_details.get("failureReason")) + elif execution_details.get("isError"): self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device Deletion get failed because of {0}".format(failure_reason) + else: + self.msg = "Device Deletion get failed." + self.log(self.msg) break return self From d9b924997bdaa5dbd7fe9041170c3ea2b78fdd5c Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 13 Dec 2023 17:07:25 +0530 Subject: [PATCH 38/50] fix sanity test issue of space around not operator --- plugins/modules/inventory_intent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 720a9eb3f1..aa8c6ab522 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -760,7 +760,7 @@ def export_device_details(self): device_ips = self.config[0].get("ip_address", []) - if not(device_ips): + if not device_ips: msg = "No Devices are given in the playbook so can't export device details" self.status = "failed" self.msg = msg @@ -878,7 +878,7 @@ def reboot_access_points(self): device_ips = self.config[0].get("ip_address", []) - if not(device_ips): + if not device_ips: msg = "No AP Devices IP given in the playbook so can't perform reboot operation" self.status = "failed" self.msg = msg From 95d53bb20230b5ea223a6df40054c6dc9edc71b2 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 14 Dec 2023 11:14:14 +0530 Subject: [PATCH 39/50] support multiple operation in single run, seperate API for resync device --- plugins/modules/inventory_intent.py | 285 ++++++++++++++-------------- 1 file changed, 145 insertions(+), 140 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index aa8c6ab522..0c4b702fe6 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -255,6 +255,7 @@ snmp_username: string snmp_version: string type: string + device_added: true update_mgmt_ipaddresslist: - exist_mgmt_ipaddress: string new_mgmt_ipaddress: string @@ -285,6 +286,7 @@ snmp_timeout: 5 snmp_username: string username: string + device_added: true type: "COMPUTE_DEVICE" - name: Add new Meraki device in Inventory with full credentials.Inputs needed for Meraki Device. @@ -300,6 +302,7 @@ state: merged config: - http_password: string + device_added: true type: "MERAKI_DASHBOARD" - name: Add new Firepower Management device in Inventory with full credentials.Input needed to add Device. @@ -318,6 +321,7 @@ http_username: string http_password: string http_port: string + device_added: true type: "FIREPOWER_MANAGEMENT_SYSTEM" - name: Add new Third Party device in Inventory with full credentials.Input needed to add Device. @@ -341,6 +345,7 @@ snmp_retry: 3 snmp_timeout: 5 snmp_username: string + device_added: true type: "THIRD_PARTY_DEVICE" - name: Associate Wired Devices to site and Provisioned it in Inventory @@ -446,8 +451,8 @@ state: merged config: - ip_address: string - device_resync: True - force_sync: False + device_resync: true + force_sync: false - name: Reboot AP Devices with IP Addresses cisco.dnac.inventory_intent: @@ -462,7 +467,7 @@ state: merged config: - ip_address: string - reboot_device: True + reboot_device: true - name: Delete Provision/Unprovisioned Devices by IP Address cisco.dnac.inventory_intent: @@ -577,6 +582,7 @@ def validate_input(self): 'update_mgmt_ipaddresslist': {'type': 'list', 'elements': 'dict'}, 'username': {'type': 'str'}, 'update_device_role': {'type': 'dict'}, + 'device_added': {'type': 'bool'}, 'device_resync': {'type': 'bool'}, 'force_sync': {'type': 'bool'}, 'clean_config': {'type': 'bool'}, @@ -854,7 +860,6 @@ def export_device_details(self): self.log(msg) self.status = "success" self.result['changed'] = True - self.result['log'].append(msg) except Exception as e: self.msg = "Cannot Export the Device Details into CSV file for {0}".format(str(device_ips)) @@ -863,6 +868,76 @@ def export_device_details(self): return self + def resync_devices(self): + """ + Resync devices in Cisco DNA Center. + This function performs the Resync operation for the devices specified in the playbook. + Parameters: + self (object): An instance of a class used for interacting with Cisco DNA Center. + Returns: + self (object): An instance of a class used for interacting with Cisco DNA Center. + Description: + The function expects the following parameters in the configuration: + - "ip_address": List of device IP addresses to be resynced. + - "force_sync": (Optional) Whether to force sync the devices. Defaults to "False". + """ + + # Code for triggers the resync operation using the retrieved device IDs and force sync parameter. + device_ips = self.config[0].get("ip_address", []) + + if not device_ips: + msg = "Cannot perform the Resync operation as device's are not present in Cisco DNA Center" + self.status = "failed" + self.msg = msg + self.log(msg) + return self + + device_ids = self.get_device_ids(device_ips) + + try: + force_sync = self.config[0].get("force_sync", "False") + resync_param_dict = { + 'payload': device_ids, + 'force_sync': force_sync + } + response = self.dnac._exec( + family="devices", + function='sync_devices_using_forcesync', + op_modifies=True, + params=resync_param_dict, + ) + self.log(str(response)) + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + + while True: + execution_details = self.get_task_details(task_id) + + if 'Synced' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device Resynced get failed because of {0}".format(failure_reason) + else: + self.msg = "Device Resynced get failed." + self.log(self.msg) + break + self.log("Device Resynced Successfully and Resynced devices are :" + str(device_ips)) + msg = "Device " + str(device_ips) + " Resynced Successfully !!" + + except Exception as e: + error_message = "Error while Resyncing device in Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) + raise Exception(error_message) + + return self + def reboot_access_points(self): """ Reboot access points in Cisco DNA Center. @@ -940,9 +1015,8 @@ def reboot_access_points(self): self.log("AP Devices Rebooted Successfully and Rebooted devices are :" + str(device_ips)) msg = "Device " + str(device_ips) + " Rebooted Successfully !!" - self.result['log'].append(msg) - return self + return self def provisioned_wired_device(self): """ @@ -1305,17 +1379,9 @@ def get_diff_merged(self, config): devices_to_add = self.have["device_not_in_dnac"] device_type = self.config[0].get("type", "NETWORK_DEVICE") device_resynced = self.config[0].get("device_resync", False) + device_added = self.config[0].get("device_added", False) device_updated = self.config[0].get("device_updated", False) device_reboot = self.config[0].get("reboot_device", False) - self.result['log'] = [] - - if device_reboot: - self.reboot_access_points() - return self - - if self.config[0].get('export_device_list'): - self.export_device_details() - return self if self.config[0].get('add_user_defined_field'): field_name = self.config[0].get('add_user_defined_field').get('name') @@ -1327,6 +1393,7 @@ def get_diff_merged(self, config): # Check if the Global User defined field exist if not then create it with given field name udf_exist = self.is_udf_exist(field_name) + if not udf_exist: # Create the Global UDF self.create_user_defined_field().check_return_status() @@ -1345,67 +1412,8 @@ def get_diff_merged(self, config): self.add_field_to_devices(device_ids).check_return_status() self.result['changed'] = True - log_msg = "Global User Defined Added with name {0} added to device Successfully !".format(field_name) - log(log_msg) - self.result['log'].append(log_msg) - - return self - - if device_resynced: - # Code for triggers the resync operation using the retrieved device IDs and force sync parameter. - device_ips = config.get("ip_address") - device_ids = self.get_device_ids(device_ips) - - if len(device_ids) == 0: - self.msg = "Cannot perform the Resync operation as device's are not present in Cisco DNA Center" - self.status = "failed" - self.result['changed'] = False - return self - - try: - force_sync = self.config[0].get("force_sync", "False") - resync_param_dict = { - 'payload': device_ids, - 'force_sync': force_sync - } - response = self.dnac._exec( - family="devices", - function='sync_devices_using_forcesync', - op_modifies=True, - params=resync_param_dict, - ) - self.log(str(response)) - - if response and isinstance(response, dict): - task_id = response.get('response').get('taskId') - - while True: - execution_details = self.get_task_details(task_id) - - if 'Synced' in execution_details.get("progress"): - self.status = "success" - self.result['changed'] = True - self.result['response'] = execution_details - break - elif execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Device Resynced get failed because of {0}".format(failure_reason) - else: - self.msg = "Device Resynced get failed." - self.log(self.msg) - break - self.log("Device Resynced Successfully and Resynced devices are :" + str(device_ips)) - msg = "Device " + str(device_ips) + " Resynced Successfully !!" - self.result['log'].append(msg) - - return self - - except Exception as e: - error_message = "Error while Resyncing device in Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) - raise Exception(error_message) + self.msg = "Global User Defined Added with name {0} added to device Successfully !".format(field_name) + self.log(self.msg) config['type'] = device_type if device_type == "FIREPOWER_MANAGEMENT_SYSTEM": @@ -1450,7 +1458,6 @@ def get_diff_merged(self, config): self.status = "success" self.result['changed'] = False log_msg = "Device Role - {0} same in Cisco DNA Center as well, no updation needed".format(device_role_args.get('role')) - self.result['log'] = log_msg continue device_role_params = { @@ -1480,7 +1487,6 @@ def get_diff_merged(self, config): self.result['response'] = execution_details log("Device Role Updated Successfully") msg = "Device " + str(device_to_update) + " Role updated Successfully !!" - self.result['log'].append(msg) break elif execution_details.get("isError"): self.status = "failed" @@ -1497,8 +1503,6 @@ def get_diff_merged(self, config): self.log(error_message) raise Exception(error_message) - return self - if self.config[0].get('update_interface_details'): # Call the Get interface details by device IP API and fetch the interface Id for device_ip in device_to_update: @@ -1540,9 +1544,8 @@ def get_diff_merged(self, config): self.status = "success" self.result['changed'] = True self.result['response'] = execution_details - log_msg = "Update Interface Details for device {0} Added Successfully".format(device_ip) - log(log_msg) - self.result['log'].append(log_msg) + self.msg = "Update Interface Details for device {0} Added Successfully".format(device_ip) + self.log(self.msg) break elif execution_details.get("isError"): self.status = "failed" @@ -1558,10 +1561,8 @@ def get_diff_merged(self, config): error_message = "Error while Updating Interface Details in Cisco DNA Center - {0}".format(str(e)) self.status = "success" self.result['changed'] = False - log_msg = "Port actions are only supported on user facing/access ports as it's not allowed or No Updation required" - self.result['log'] = log_msg - - return self + self.msg = "Port actions are only supported on user facing/access ports as it's not allowed or No Updation required" + self.log(msg) # Update Device details and credentails try: @@ -1596,73 +1597,78 @@ def get_diff_merged(self, config): self.log(self.msg) break - log("Device Updated Successfully") - log("Updated devices are :" + str(device_to_update)) - msg = "Device " + str(device_to_update) + " updated Successfully !!" - self.result['log'].append(msg) + self.log("Device Updated Successfully") + self.log("Updated devices are :" + str(device_to_update)) + self.msg = "Device " + str(device_to_update) + " updated Successfully !!" + self.log(self.msg) except Exception as e: error_message = "Error while Updating device in Cisco DNA Center - {0}".format(str(e)) self.log(error_message) raise Exception(error_message) - msg = "Devices {0} present in Cisco DNA Center and updated successfully".format(config['ip_address']) - self.log(msg) - self.result['log'].append(msg) + self.msg = "Devices {0} present in Cisco DNA Center and updated successfully".format(config['ip_address']) + self.log(self.msg) self.status = "success" - return self - # If we want to add device in inventory - config['ip_address'] = devices_to_add - self.mandatory_parameter().check_return_status() - try: - response = self.dnac._exec( - family="devices", - function='add_device', - op_modifies=True, - params=self.want.get("device_params"), - ) - self.log(str(response)) + if device_added: + config['ip_address'] = devices_to_add + self.mandatory_parameter().check_return_status() + try: + response = self.dnac._exec( + family="devices", + function='add_device', + op_modifies=True, + params=self.want.get("device_params"), + ) + self.log(str(response)) - if response and isinstance(response, dict): - task_id = response.get('response').get('taskId') + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') - while True: - execution_details = self.get_task_details(task_id) + while True: + execution_details = self.get_task_details(task_id) - if '/task/' in execution_details.get("progress"): - self.status = "success" - self.result['response'] = execution_details + if '/task/' in execution_details.get("progress"): + self.status = "success" + self.result['response'] = execution_details - if len(devices_to_add) > 0: - self.result['changed'] = True - log("Device Added Successfully") - log("Added devices are :" + str(devices_to_add)) - msg = "Device " + str(devices_to_add) + " added Successfully !!" - self.result['log'].append(msg) + if len(devices_to_add) > 0: + self.result['changed'] = True + log("Device Added Successfully") + log("Added devices are :" + str(devices_to_add)) + msg = "Device " + str(devices_to_add) + " added Successfully !!" + break + msg = "Devices " + str(self.config[0].get("ip_address")) + " already present in Cisco DNA Center" + break + elif execution_details.get("isError"): + self.status = "failed" + failure_reason = execution_details.get("failureReason") + if failure_reason: + self.msg = "Device Addition get failed because of {0}".format(failure_reason) + else: + self.msg = "Device Addition get failed" + self.log(self.msg) break - msg = "Devices " + str(self.config[0].get("ip_address")) + " already present in Cisco DNA Center" - self.result['log'].append(msg) - break - elif execution_details.get("isError"): - self.status = "failed" - failure_reason = execution_details.get("failureReason") - if failure_reason: - self.msg = "Device Addition get failed because of {0}".format(failure_reason) - else: - self.msg = "Device Addition get failed" - self.log(self.msg) - break - except Exception as e: - error_message = "Error while Adding device in Cisco DNA Center - {0}".format(str(e)) - self.log(error_message) - raise Exception(error_message) + except Exception as e: + error_message = "Error while Adding device in Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) + raise Exception(error_message) # Once device get added we will assign device to site and Provisioned it if self.config[0].get('provision_wired_device'): - self.provisioned_wired_device() + self.provisioned_wired_device().check_return_status() + + if device_resynced: + self.resync_devices().check_return_status() + + if device_reboot: + self.reboot_access_points().check_return_status() + + if self.config[0].get('export_device_list'): + self.export_device_details().check_return_status() return self @@ -1692,7 +1698,7 @@ def get_diff_deleted(self, config): self.msg = msg self.status = "success" self.result['changed'] = False - self.result['msg'].append(msg) + self.result['msg'] = msg return self try: @@ -1733,11 +1739,10 @@ def get_diff_deleted(self, config): for device_ip in device_to_delete: if device_ip not in self.have.get("device_in_dnac"): self.result['changed'] = False - msg = "The device {0} is not present in Cisco DNA Center so can't perform delete operation".format(device_ip) - self.msg = msg + self.msg = "The device {0} is not present in Cisco DNA Center so can't perform delete operation".format(device_ip) self.status = "success" self.result['changed'] = False - self.result['msg'].append(msg) + self.result['msg'] = self.msg continue try: From e59477798c7e1761b30f552851bb022c57a5dbac Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Thu, 14 Dec 2023 11:36:13 +0000 Subject: [PATCH 40/50] Adding code for provisioning (wired is validated) --- playbooks/device_provision.yml | 30 ++ plugins/modules/provision_intent.py | 522 ++++++++++++++++++++++++++++ 2 files changed, 552 insertions(+) create mode 100644 playbooks/device_provision.yml create mode 100644 plugins/modules/provision_intent.py diff --git a/playbooks/device_provision.yml b/playbooks/device_provision.yml new file mode 100644 index 0000000000..ed780ed5ab --- /dev/null +++ b/playbooks/device_provision.yml @@ -0,0 +1,30 @@ +--- +- name: Provision and Re-provision wired and wireless devices + hosts: localhost + connection: local + gather_facts: no + + vars_files: + - "{{ CLUSTERFILE }}" + + vars: + dnac_login: &dnac_login + 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 }}" + + tasks: + - name: Provision a wired device to a site + cisco.dnac.provision_intent: + <<: *dnac_login + dnac_log: True + state: merged + config: + - site_name: Global/USA/San Francisco/BGL_18 + management_ip_address: 204.1.2.2 + + \ No newline at end of file diff --git a/plugins/modules/provision_intent.py b/plugins/modules/provision_intent.py new file mode 100644 index 0000000000..5da4db5e36 --- /dev/null +++ b/plugins/modules/provision_intent.py @@ -0,0 +1,522 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2021, Cisco Systems +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = ("Abinash Mishra") + +DOCUMENTATION = r""" +--- +module: provision_intent +short_description: Resource module for provision functions +description: +- Manage operation related to wired and wireless provisioning +- API to re-provision provisioned devices +- API to un-provision provisioned devices +version_added: '6.6.0' +extends_documentation_fragment: + - cisco.dnac.intent_params +author: Abinash Mishra (@abimishr) +options: + state: + description: The state of DNAC after module completion. + type: str + choices: [ merged, deleted ] + default: merged + config: + description: + - List of details of device being managed. + type: list + elements: dict + required: true + suboptions: + management_ip_address: + description: Management Ip Address . + type: str + required: true + site_name: + 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 + type: list + elements: str + dynamic_interfaces: + description: Interface details of the controller + type: list + elements: dict + suboptions: + interface_ip_address: + description: Ip Address allocated to the interface + type: str + interface_netmask_in_c_i_d_r: + description: Ip Address allocated to the interface + type: int + interface_gateway: + description: Ip Address allocated to the interface + type: str + lag_or_port_number: + description: Ip Address allocated to the interface + type: int + vlan_id: + description: Ip Address allocated to the interface + type: int + interface_name: + description: Ip Address allocated to the interface + type: str + +requirements: +- dnacentersdk == 2.4.5 +- python >= 3.5 +notes: + - SDK Methods used are + sites.Sites.get_site, + devices.Devices.get_network_device_by_ip, + task.Task.get_task_by_id, + sda.Sda.get_provisioned_wired_device, + sda.Sda.re_provision_wired_device, + sda.Sda.provision_wired_device, + wireless.Wireless.provision + + - Paths used are + get /dna/intent/api/v1/site + get /dna/intent/api/v1/network-device/ip-address/{ipAddress} + get /dna/intent/api/v1/task/{taskId} + get /dna/intent/api/v1/business/sda/provision-device + put /dna/intent/api/v1/business/sda/provision-device + post /dna/intent/api/v1/business/sda/provision-device + post /dna/intent/api/v1/wireless/provision + +""" + +EXAMPLES = r""" +- name: Create/Modify a new provision + cisco.dnac.provision_intent: + dnac_host: "{{dnac_host}}" + dnac_username: "{{dnac_username}}" + dnac_password: "{{dnac_password}}" + dnac_verify: "{{dnac_verify}}" + dnac_port: "{{dnac_port}}" + dnac_version: "{{dnac_version}}" + dnac_debug: "{{dnac_debug}}" + dnac_log: True + state: merged + config: + - site_name: string + management_ip_address: string + managed_ap_locations: list + 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 + +""" + +RETURN = r""" +# Case_1: Successful creation/updation/deletion of provision +response_1: + description: A dictionary with details of provision is returned + returned: always + type: dict + sample: > + { + "response": + { + "response": String, + "version": String + }, + "msg": String + } + +# Case_2: Error while creating a provision +response_2: + description: A list with the response returned by the Cisco DNAC Python SDK + returned: always + type: list + sample: > + { + "response": [], + "msg": String + } + +# Case_3: Already exists and requires no update +response_3: + description: A dictionary with the exisiting details as returned by the Cisco DNAC Python SDK + returned: always + type: dict + sample: > + { + "response": String, + "msg": String + } +""" +import time +import re +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( + DnacBase, + validate_list_of_dicts +) + + +class Dnacprovision(DnacBase): + + """ + Class containing member attributes for provision intent module + """ + def __init__(self, module): + super().__init__(module) + + def validate_input(self): + + """ + Validate the fields provided in the playbook. + Checks the configuration provided in the playbook against a predefined specification + to ensure it adheres to the expected structure and data types. + Args: + 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: + - self.msg: A message describing the validation result. + - self.status: The status of the validation (either 'success' or 'failed'). + - self.validated_config: If successful, a validated version of the + 'config' parameter. + Example: + To use this method, create an instance of the class and call 'validate_input' on it. + If the validation succeeds, 'self.status' will be 'success' and + 'self.validated_config' will contain the validated configuration. If it fails, + 'self.status' will be 'failed', and 'self.msg' will describe the validation issues. + """ + + if not self.config: + self.msg = "config not available in playbook for validattion" + self.status = "success" + return self + + provision_spec = { + "management_ip_address": {'type': 'str', 'required': True}, + "site_name": {'type': 'str', 'required': True}, + "managed_ap_locations": {'type': 'list', 'required': False, + 'elements': 'str'}, + "dynamic_interfaces": {'type': 'list', 'required': False, + 'elements': 'dict'} + } + # Validate provision params + valid_provision, invalid_params = validate_list_of_dicts( + self.config, provision_spec + ) + if invalid_params: + self.msg = "Invalid parameters in playbook: {0}".format( + "\n".join(invalid_params)) + self.status = "failed" + return self + + self.validated_config = valid_provision + self.log(str(valid_provision)) + self.msg = "Successfully validated input" + self.status = "success" + return self + + def get_dev_type(self): + """ + Fetches the type of device (wired/wireless) + + 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: + - device_type: A string indicating the type of the + device (wired/wireless). + Example: + 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"]} + ) + + dev_dict = dev_response.get("response") + device_family = dev_dict["family"] + if device_family == "Wireless Controller": + device_type = "wireless" + elif device_family in ["Switches and Hubs", "Routers"]: + device_type = "wired" + return device_type + + def get_task_status(self, task_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 = {"task_id": task_id} + while True: + response = self.dnac_apply['exec']( + family="task", + function='get_task_by_id', + params=params, + ) + response = response.response + if response.get('isError') or re.search( + 'failed', response.get('progress'), flags=re.IGNORECASE + ): + msg = 'Discovery task with id {0} has not completed - Reason: {1}'.format( + task_id, response.get("failureReason")) + self.module.fail_json(msg=msg) + return False + + if response.get('progress') != 'In Progress': + result = True + break + time.sleep(3) + + self.result.update(dict(discovery_task=response)) + return result + + def get_site_type(self, site_name=None): + """ + Fetches the type of 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_type: A string indicating the type of the + site (area/building/floor). + Example: + Post creation of the validated input, we this method gets the + type of the site. + """ + + try: + response = self.dnac_apply['exec']( + family="sites", + function='get_site', + params={"name": site_name}, + ) + except Exception: + self.module.fail_json(msg="Site not found", response=[]) + + if response: + self.log(str(response)) + site = response.get("response") + site_additional_info = site[0].get("additionalInfo") + for item in site_additional_info: + if item["nameSpace"] == "Location": + site_type = item.get("attributes").get("type") + + return site_type + + def get_wired_params(self): + + wired_params = { + "deviceManagementIpAddress": self.validated_config[0]["management_ip_address"], + "siteNameHierarchy": self.validated_config[0].get("site_name") + } + + return wired_params + + def get_wireless_params(self): + wireless_params = [ + { + "site": self.validated_config[0].get("site_name"), + "managedAPLocations": self.validated_config[0].get("managed_ap_locations"), + } + ] + for ap_loc in wireless_params[0]["managedAPLocations"]: + if self.get_site_type(site_name=ap_loc) != "floor": + 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) + response = self.dnac_apply['exec']( + family="devices", + function='get_network_device_by_ip', + params={"management_ip_address": self.validated_config[0]["management_ip_address"]} + ) + + wireless_params[0]["deviceName"] = response.get("response")[0].get("hostname") + return wireless_params + + def get_want(self): + + """ + Get all provision related informantion from the playbook + Args: + self: The instance of the class containing the 'config' attribute to be validated. + config: validated config passed from the playbook + Returns: + The method returns an instance of the class with updated attributes: + - self.want: A dictionary of paramters obtained from the playbook + - self.msg: A message indicating all the paramters from the playbook are + collected + - self.status: Success + Example: + It stores all the paramters passed from the playbook for further processing + before calling the APIs + """ + + self.want = {} + self.want["device_type"] = self.get_dev_type() + if self.want["device_type"] == "wired": + self.want["prov_params"] = self.get_wired_params() + elif self.want["device_type"] == "wireless": + self.want["prov_params"] = self.get_wireless_params() + self.msg = "Successfully collected all parameters from playbook " + \ + "for comparison" + self.status = "success" + return self + + def get_diff_merged(self): + + """ + Add to provision database + Args: + self: An instance of a class used for interacting with Cisco DNA Center. + Returns: + object: An instance of the class with updated results and status + based on the processing of differences. + Description: + The function processes the differences and, depending on the + changes required, it may add, update,or resynchronize devices in + Cisco DNA Center. The updated results and status are stored in the + class instance for further use. + """ + if self.want.get("device_type") == "wired": + try: + status_response = self.dnac_apply['exec']( + family="sda", + function="get_provisioned_wired_device", + op_modifies=True, + params={ + "device_management_\ + ip_address": + self.validated_config[0]["management_ip_address"] + }, + ) + except Exception: + status_response = {} + + status = status_response.get("status") + + if status == "success": + response = response = self.dnac_apply['exec']( + family="sda", + function="re_provision_wired_device", + op_modifies=True, + params=self.want["prov_params"], + ) + else: + response = self.dnac_apply['exec']( + family="sda", + function="provision_wired_device", + op_modifies=True, + params=self.want["prov_params"], + ) + + elif self.want.get("device_type") == "wireless": + response = self.dnac_apply['exec']( + family="wireless", + function="provision", + op_modifies=True, + params=self.want["prov_params"], + ) + + task_id = response.get("taskId") + provision_info = self.get_task_status(task_id=task_id) + self.result["changed"] = True + self.result['msg'] = "Provision done Successfully" + self.result['diff'] = self.validated_config + self.result['response'] = task_id + + return self + + def get_diff_deleted(self): + + """ + Delete from provision database + Args: + self: An instance of a class used for interacting with Cisco DNA Center + Returns: + self: An instance of the class with updated results and status based on + the deletion operation. + Description: + This function is responsible for removing devices from the Cisco DNA Center PnP GUI and + raise Exception if any error occured. + """ + + pass + + return self + + +def main(): + + """ + main entry point for module execution + """ + + element_spec = {'dnac_host': {'required': True, 'type': 'str'}, + 'dnac_port': {'type': 'str', 'default': '443'}, + 'dnac_username': {'type': 'str', 'default': 'admin', 'aliases': ['user']}, + 'dnac_password': {'type': 'str', 'no_log': True}, + 'dnac_verify': {'type': 'bool', 'default': 'True'}, + 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, + 'dnac_debug': {'type': 'bool', 'default': False}, + 'dnac_log': {'type': 'bool', 'default': False}, + 'validate_response_schema': {'type': 'bool', 'default': True}, + 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, + 'state': {'default': 'merged', 'choices': ['merged', 'deleted']} + } + module = AnsibleModule(argument_spec=element_spec, + supports_check_mode=False) + dnac_provision = Dnacprovision(module) + + state = dnac_provision.params.get("state") + if state not in dnac_provision.supported_states: + dnac_provision.status = "invalid" + dnac_provision.msg = "State {0} is invalid".format(state) + dnac_provision.check_return_status() + + dnac_provision.validate_input().check_return_status() + + for config in dnac_provision.validated_config: + dnac_provision.reset_values() + dnac_provision.get_want().check_return_status() + dnac_provision.get_diff_state_apply[state]().check_return_status() + + module.exit_json(**dnac_provision.result) + + +if __name__ == '__main__': + main() From 10dc4b3d14f1dd16888c196801daad0ecf82ea89 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Mon, 18 Dec 2023 04:58:30 +0000 Subject: [PATCH 41/50] Adding code for Wired and wireless provisioning --- plugins/modules/provision_intent.py | 52 ++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/plugins/modules/provision_intent.py b/plugins/modules/provision_intent.py index 5da4db5e36..690b1522e8 100644 --- a/plugins/modules/provision_intent.py +++ b/plugins/modules/provision_intent.py @@ -247,10 +247,13 @@ def get_dev_type(self): dev_dict = dev_response.get("response") device_family = dev_dict["family"] + if device_family == "Wireless Controller": device_type = "wireless" elif device_family in ["Switches and Hubs", "Routers"]: device_type = "wired" + else: + device_type = None return device_type def get_task_status(self, task_id=None): @@ -288,8 +291,8 @@ def get_task_status(self, task_id=None): if response.get('progress') != 'In Progress': result = True break - time.sleep(3) + time.sleep(3) self.result.update(dict(discovery_task=response)) return result @@ -330,6 +333,22 @@ def get_site_type(self, site_name=None): def get_wired_params(self): + """ + Prepares the payload for provisioning of the wired devices + + 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: + - wired_params: A dictionary containing all the values indicating + management IP address of the device and the hierarchy + of the site. + Example: + Post creation of the validated input, it fetches the required + paramters and stores it for further processing and calling the + parameters in other APIs. + """ wired_params = { "deviceManagementIpAddress": self.validated_config[0]["management_ip_address"], "siteNameHierarchy": self.validated_config[0].get("site_name") @@ -338,6 +357,24 @@ def get_wired_params(self): return wired_params def get_wireless_params(self): + + """ + Prepares the payload for provisioning of the wireless devices + + 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: + - wireless_params: A list of dictionary containing all the values indicating + management IP address of the device, hierarchy + of the site, AP Location of the wireless controller and details + of the interface + Example: + Post creation of the validated input, it fetches the required + paramters and stores it for further processing and calling the + parameters in other APIs. + """ wireless_params = [ { "site": self.validated_config[0].get("site_name"), @@ -385,13 +422,15 @@ def get_want(self): It stores all the paramters passed from the playbook for further processing before calling the APIs """ - self.want = {} self.want["device_type"] = self.get_dev_type() if self.want["device_type"] == "wired": self.want["prov_params"] = self.get_wired_params() elif self.want["device_type"] == "wireless": self.want["prov_params"] = self.get_wireless_params() + else: + self.log("Passed devices are neither wired or wireless devices") + self.msg = "Successfully collected all parameters from playbook " + \ "for comparison" self.status = "success" @@ -412,7 +451,8 @@ def get_diff_merged(self): Cisco DNA Center. The updated results and status are stored in the class instance for further use. """ - if self.want.get("device_type") == "wired": + device_type = self.want.get("device_type") + if device_type == "wired": try: status_response = self.dnac_apply['exec']( family="sda", @@ -444,7 +484,7 @@ class instance for further use. params=self.want["prov_params"], ) - elif self.want.get("device_type") == "wireless": + elif device_type == "wireless": response = self.dnac_apply['exec']( family="wireless", function="provision", @@ -452,6 +492,10 @@ class instance for further use. params=self.want["prov_params"], ) + else: + self.result['msg'] = "Passed device is neither wired nor wireless" + self.result['response'] = self.want["prov_params"] + task_id = response.get("taskId") provision_info = self.get_task_status(task_id=task_id) self.result["changed"] = True From 90a6cde7b04c5dd566741cb3d9a00cd2583d0c88 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Mon, 18 Dec 2023 17:39:48 +0530 Subject: [PATCH 42/50] validate password with regex, export device credential into csv, add modified playbook for inventory --- playbooks/inventory_device.yml | 14 ++++ plugins/module_utils/dnac.py | 22 ++++++ plugins/modules/inventory_intent.py | 108 +++++++++++++++++++--------- 3 files changed, 112 insertions(+), 32 deletions(-) diff --git a/playbooks/inventory_device.yml b/playbooks/inventory_device.yml index 8a28c864a4..721156a808 100644 --- a/playbooks/inventory_device.yml +++ b/playbooks/inventory_device.yml @@ -34,6 +34,20 @@ device_updated: "{{item.device_updated}}" clean_config: "{{item.clean_config}}" type: "{{item.type}}" + device_updated: "{{item.device_updated}}" + update_device_role: + role: "{{item.role}}" + role_source: "{{item.role_source}}" + add_user_defined_field: + name: "{{item.name}}" + description: "{{item.description}}" + value: "{{item.value}}" + provision_wired_device: + site_name: "{{item.site_name}}" + export_device_list: + password: "{{item.export_device_list.password}}" + operation_enum: "{{item.export_device_list.operation_enum}}" + reboot_device: "{{item.reboot_device}}" with_items: "{{ device_details }}" tags: diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 98290e5dd1..aec0d81c35 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -25,6 +25,7 @@ import copy import datetime import inspect +import re class DnacBase(): @@ -118,6 +119,27 @@ def check_return_status(self): elif "invalid" in self.status: self.module.fail_json(msg=self.msg, response=[]) + def is_valid_password(self, password): + """ + Check if a password is valid. + Args: + self (object): An instance of a class that provides access to Cisco DNA Center. + password (str): The password to be validated. + Returns: + bool: True if the password is valid, False otherwise. + Description: + The function checks the validity of a password based on the following criteria: + - Minimum 8 characters. + - At least one lowercase letter. + - At least one uppercase letter. + - At least one digit. + - At least one special character + """ + + pattern = r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[-=\\;,./~!@#$%^&*()_+{}[\]|:?]).{8,}$" + + return re.match(pattern, password) is not None + def get_dnac_params(self, params): """Store the DNAC parameters from the playbook""" diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 0c4b702fe6..41fac82717 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -517,10 +517,17 @@ "version": "string" } """ +# common approach when a module relies on optional dependencies that are not available during the validation process. +try: + import pyzipper + HAS_PYZIPPER = True +except ImportError: + HAS_PYZIPPER = False + pyzipper = None import csv from datetime import datetime -from io import StringIO +from io import BytesIO, StringIO from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( DnacBase, @@ -589,7 +596,8 @@ def validate_input(self): 'add_user_defined_field': {'type': 'dict'}, 'upate_interface_details': {'type': 'dict'}, 'deployment_mode': {'default': 'Deploy', 'type': 'str'}, - 'provision_wired_device': {'type': 'dict'} + 'provision_wired_device': {'type': 'dict'}, + 'export_device_list': {'type': 'dict'} } # Validate device params @@ -760,7 +768,8 @@ def export_device_details(self): self (object): An instance of the class with updated result, status, and log. Description: This function exports device details from Cisco DNA Center based on the provided IP addresses in the configuration. - It retrieves the device UUIDs, calls the export device list API, and downloads the exported data in CSV format. + It retrieves the device UUIDs, calls the export device list API, and downloads the exported data of both device details and + and device credentials with an encrtypted zip file with password into CSV format. The CSV data is then parsed and written to a file. """ @@ -773,17 +782,8 @@ def export_device_details(self): self.log(msg) return self - device_uuids = [] try: - for device_ip in device_ips: - response = self.dnac._exec( - family="devices", - function='get_device_list', - params={"managementIpAddress": device_ip} - ) - response = response.get('response') - if response: - device_uuids.append(response[0]["id"]) + device_uuids = self.get_device_ids(device_ips) if not device_uuids: self.status = "failed" @@ -794,10 +794,22 @@ def export_device_details(self): # Now all device UUID get collected so call the export device list API export_device_list = self.config[0].get('export_device_list') + password = export_device_list.get("password") + + if not self.is_valid_password(password): + self.status = "failed" + self.result['changed'] = False + detailed_msg = """Invalid password. Min password length is 8 and it should contain atleast one lower case letter, + one uppercase letter, one digit and one special characters from -=\\;,./~!@#$%^&*()_+{}[]|:?""" + formatted_msg = ' '.join(line.strip() for line in detailed_msg.splitlines()) + self.msg = formatted_msg + self.log(formatted_msg) + return self + payload_params = { "deviceUuids": device_uuids, - "password": export_device_list.get("password"), - "operationEnum": export_device_list.get("operation_enum", "1"), + "password": password, + "operationEnum": export_device_list.get("operation_enum", "0"), "paramters": export_device_list.get("paramters") } @@ -828,7 +840,7 @@ def export_device_details(self): return self - # With this File ID call the Download File by FileID API + # With this File ID call the Download File by FileID API and process the response response = self.dnac._exec( family="file", function='download_a_file_by_fileid', @@ -836,28 +848,60 @@ def export_device_details(self): params={"file_id": file_id}, ) - device_data = [] - encoded_resp = response.data.decode(encoding='utf-8') - self.log(str(encoded_resp)) + if payload_params["operationEnum"] == "0": + zip_data = BytesIO(response.data) + + if HAS_PYZIPPER: + # Create a PyZipper object with the password + with pyzipper.AESZipFile(zip_data, 'r', compression=pyzipper.ZIP_LZMA, encryption=pyzipper.WZ_AES) as zip_ref: + # Assuming there is a single file in the zip archive + file_name = zip_ref.namelist()[0] + + # Extract the content of the file with the provided password + file_content_binary = zip_ref.read(file_name, pwd=password.encode('utf-8')) + + # Now 'file_content_binary' contains the binary content of the decrypted file + # Since the content is text, so we can decode it + file_content_text = file_content_binary.decode('utf-8') + + # Now 'file_content_text' contains the text content of the decrypted file + self.log(file_content_text) + + # Parse the CSV-like string into a list of dictionaries + csv_reader = csv.DictReader(StringIO(file_content_text)) + temp_file_name = response.filename + output_file_name = temp_file_name.split(".")[0] + ".csv" - # Parse the CSV-like string into a list of dictionaries - csv_reader = csv.DictReader(StringIO(encoded_resp)) + else: + self.msg = "pyzipper is required for this module. Install pyzipper to use this functionality." + self.log(self.msg) + self.status = "failed" + + return self + + else: + encoded_resp = response.data.decode(encoding='utf-8') + self.log(str(encoded_resp)) + + # Parse the CSV-like string into a list of dictionaries + csv_reader = csv.DictReader(StringIO(encoded_resp)) + current_date = datetime.now() + formatted_date = current_date.strftime("%m-%d-%Y") + output_file_name = "devices-" + str(formatted_date) + ".csv" + + device_data = [] for row in csv_reader: device_data.append(row) - current_date = datetime.now() - formatted_date = current_date.strftime("%m-%d-%Y") - file_name = "devices-" + str(formatted_date) + ".csv" - # Write the data to a CSV file - with open(file_name, 'w', newline='') as csv_file: + with open(output_file_name, 'w', newline='') as csv_file: fieldnames = device_data[0].keys() csv_writer = csv.DictWriter(csv_file, fieldnames=fieldnames) csv_writer.writeheader() csv_writer.writerows(device_data) - msg = "Device Details Exported Successfully to the CSV file - {0}".format(file_name) - self.log(msg) + self.msg = "Device Details Exported Successfully to the CSV file - {0}".format(output_file_name) + self.log(self.msg) self.status = "success" self.result['changed'] = True @@ -954,10 +998,10 @@ def reboot_access_points(self): device_ips = self.config[0].get("ip_address", []) if not device_ips: - msg = "No AP Devices IP given in the playbook so can't perform reboot operation" - self.status = "failed" - self.msg = msg - self.log(msg) + self.msg = "No AP Devices IP given in the playbook so can't perform reboot operation" + self.status = "success" + self.result['changed'] = False + self.log(self.msg) return self ap_mac_address_list = [] From 3a2a535b9cb2b422b653bdbb494b9dc93a33d202 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Mon, 18 Dec 2023 17:49:01 +0530 Subject: [PATCH 43/50] remove duplicate key device_updated from playbook --- playbooks/inventory_device.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/playbooks/inventory_device.yml b/playbooks/inventory_device.yml index 721156a808..e17bf67911 100644 --- a/playbooks/inventory_device.yml +++ b/playbooks/inventory_device.yml @@ -34,7 +34,6 @@ device_updated: "{{item.device_updated}}" clean_config: "{{item.clean_config}}" type: "{{item.type}}" - device_updated: "{{item.device_updated}}" update_device_role: role: "{{item.role}}" role_source: "{{item.role_source}}" From 9d97067ccde52248bad9c66b1d8c38f2f3c97a46 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Mon, 18 Dec 2023 20:20:19 +0530 Subject: [PATCH 44/50] added coded to return if not HAS_PYZIPPER is True --- plugins/modules/inventory_intent.py | 41 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 41fac82717..7f2d4207ad 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -851,33 +851,32 @@ def export_device_details(self): if payload_params["operationEnum"] == "0": zip_data = BytesIO(response.data) - if HAS_PYZIPPER: - # Create a PyZipper object with the password - with pyzipper.AESZipFile(zip_data, 'r', compression=pyzipper.ZIP_LZMA, encryption=pyzipper.WZ_AES) as zip_ref: - # Assuming there is a single file in the zip archive - file_name = zip_ref.namelist()[0] + if not HAS_PYZIPPER: + self.msg = "pyzipper is required for this module. Install pyzipper to use this functionality." + self.log(self.msg) + self.status = "failed" - # Extract the content of the file with the provided password - file_content_binary = zip_ref.read(file_name, pwd=password.encode('utf-8')) + return self - # Now 'file_content_binary' contains the binary content of the decrypted file - # Since the content is text, so we can decode it - file_content_text = file_content_binary.decode('utf-8') + # Create a PyZipper object with the password + with pyzipper.AESZipFile(zip_data, 'r', compression=pyzipper.ZIP_LZMA, encryption=pyzipper.WZ_AES) as zip_ref: + # Assuming there is a single file in the zip archive + file_name = zip_ref.namelist()[0] - # Now 'file_content_text' contains the text content of the decrypted file - self.log(file_content_text) + # Extract the content of the file with the provided password + file_content_binary = zip_ref.read(file_name, pwd=password.encode('utf-8')) - # Parse the CSV-like string into a list of dictionaries - csv_reader = csv.DictReader(StringIO(file_content_text)) - temp_file_name = response.filename - output_file_name = temp_file_name.split(".")[0] + ".csv" + # Now 'file_content_binary' contains the binary content of the decrypted file + # Since the content is text, so we can decode it + file_content_text = file_content_binary.decode('utf-8') - else: - self.msg = "pyzipper is required for this module. Install pyzipper to use this functionality." - self.log(self.msg) - self.status = "failed" + # Now 'file_content_text' contains the text content of the decrypted file + self.log(file_content_text) - return self + # Parse the CSV-like string into a list of dictionaries + csv_reader = csv.DictReader(StringIO(file_content_text)) + temp_file_name = response.filename + output_file_name = temp_file_name.split(".")[0] + ".csv" else: encoded_resp = response.data.decode(encoding='utf-8') From 38427886db832d385a2d73ec264e084277f18b25 Mon Sep 17 00:00:00 2001 From: Madhan Date: Mon, 18 Dec 2023 22:34:10 +0530 Subject: [PATCH 45/50] Adding changeset in changelog file --- changelogs/changelog.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 0d93056462..d16f20c382 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -749,3 +749,17 @@ releases: release_summary: Changing galaxy. minor_changes: - Adding support to ansible.utils ">=2.0.0, <4.00". + 6.10.0: + release_date: "2023-12-18" + changes: + release_summary: Changes in intent module to support discovery, provisioning and more operations in swim, site, template and pnp intent modules. + minor_changes: + - Changes in inventory_intent module to support Update Device role, update interface details. + - Changes in inventory_intent module to create and assign Global User defined field to devices, Delete Global UDF, Delete Provisioned/Unprovisioned devices. + - Changes in pnp_intent module to support bulk addition and bulk deletion along with variable names. + - Handling idempotent in delete operation in site_intent module. + - Adding discovery intent module to support discovering the devices. + - Changes in swim intent module to support Distribute and activate image by fetching device based on site and device family details + - Changes in site intent module to support one-shot site deletion + - To Support provisioning wired device, reboot AP's, export device list, delete provisioned devices. + - Change the variable names into snake case in all the intent modules for better readability. From a322d076a4c2476a55f5d4cb0c7df98e4af9967f Mon Sep 17 00:00:00 2001 From: Madhan Date: Tue, 19 Dec 2023 10:22:05 +0530 Subject: [PATCH 46/50] Adding changes in galaxy file --- galaxy.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/galaxy.yml b/galaxy.yml index 74ef9df7c5..26cc840337 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,7 +1,7 @@ --- namespace: cisco name: dnac -version: 6.9.0 +version: 6.10.0 readme: README.md authors: - Rafael Campos @@ -9,8 +9,12 @@ authors: - Jose Bogarin - Bryan Vargas - Francisco Muñoz - - Madhan Sankaranarayanan (@madhansansel) + - Madhan Sankaranarayanan - Rishita Chowdhary (@rishitachowdhary) + - Muthu Rakesh Babu + - Akash Bhaskaran + - Abinash Mishra + - Abhishek Maheshwari description: Ansible Modules for Cisco DNA Center license_file: "LICENSE" tags: From ff7b6ed4db03bec64240ca9c2dad888cac3f854d Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 19 Dec 2023 12:09:08 +0530 Subject: [PATCH 47/50] Added a new hierarchy for assign device cred to a site --- playbooks/device_credential_intent.yml | 37 +++++++++++++++----------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/playbooks/device_credential_intent.yml b/playbooks/device_credential_intent.yml index 03952d168d..eef4c1f801 100644 --- a/playbooks/device_credential_intent.yml +++ b/playbooks/device_credential_intent.yml @@ -17,6 +17,7 @@ dnac_verify: "{{ dnac_verify }}" dnac_debug: "{{ dnac_debug }}" dnac_log: True + config_verify: True state: merged config: - global_credential_details: @@ -65,21 +66,27 @@ # old_username: # id: bec9818e-30cd-468b-bf75-292beefc2e20 # Use this for updation or deletion assign_credentials_to_site: - # cli_description: - # cli_username: - cli_id: e448ea13-4de0-406b-bc6e-f72b57ed6746 - # snmp_v2_read_description: - snmp_v2_read_id: 0ee7d677-8804-43f2-8b6c-599c5f18348f - # snmp_v2_write_description: - snmp_v2_write_id: a96abc1b-1fd6-41f1-8a6d-a5569c17262d - # snmp_v3_description: - snmp_v3_id: d8974823-250a-41b0-8c9b-b27b2ae01472 - # http_read_description: - # http_read_username: - http_read: d5d7af00-5a38-4ac1-9f55-03338d00c415 - # http_write_description: - # http_write_username: - http_write: bec9818e-30cd-468b-bf75-292beefc2e20 + cli_credential: + # description: CLI + # username: cli + id: e448ea13-4de0-406b-bc6e-f72b57ed6746 + snmp_v2c_read: + # description: SNMPv2c Read + id: 0ee7d677-8804-43f2-8b6c-599c5f18348f + snmp_v2c_write: + # description: SNMPv2c Write + id: a96abc1b-1fd6-41f1-8a6d-a5569c17262d + snmp_v3: + # description: snmpV3 + id: d8974823-250a-41b0-8c9b-b27b2ae01472 + https_read: + # description: HTTP Read + # username: HTTP_Read + id: d5d7af00-5a38-4ac1-9f55-03338d00c415 + https_write: + # description: HTTP Write + # username: HTTP_Write + id: bec9818e-30cd-468b-bf75-292beefc2e20 site_name: - Global/Chennai/Trill - Global/Chennai/Tidel From c10523b15f192d38fea52d483af620d72f26482e Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Tue, 19 Dec 2023 12:14:21 +0530 Subject: [PATCH 48/50] Added a new hierarchy for assign device cred to a site --- playbooks/device_credential_intent.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/playbooks/device_credential_intent.yml b/playbooks/device_credential_intent.yml index eef4c1f801..2049c577d8 100644 --- a/playbooks/device_credential_intent.yml +++ b/playbooks/device_credential_intent.yml @@ -17,7 +17,6 @@ dnac_verify: "{{ dnac_verify }}" dnac_debug: "{{ dnac_debug }}" dnac_log: True - config_verify: True state: merged config: - global_credential_details: From 345dfe461fb0b561b747bf1cd269e671e388940d Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 19 Dec 2023 16:16:48 +0530 Subject: [PATCH 49/50] change test_swim_module playbook variable in snakecase --- playbooks/test_swim_module.yml | 49 ++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/playbooks/test_swim_module.yml b/playbooks/test_swim_module.yml index 0fbbc20451..d3ca027f26 100644 --- a/playbooks/test_swim_module.yml +++ b/playbooks/test_swim_module.yml @@ -1,14 +1,14 @@ -- hosts: dnac_servers +--- +- name: Configure device credentials on Cisco DNA Center + hosts: localhost + connection: local + gather_facts: no vars_files: - - credentials_245.yml - - image_details.yml #Contains image and device details + - "input_swim.yml" #Contains image and device details + - "credentials.yml" gather_facts: false connection: local tasks: -# -# Project Info Section -# - - name: Import an image, tag it as golden and load it on device cisco.dnac.swim_intent: dnac_host: "{{ dnac_host }}" @@ -19,23 +19,26 @@ dnac_debug: "{{ dnac_debug }}" dnac_log: true config: - - importImageDetails: - type: "{{ item.import_type }}" - urlDetails: - payload: - - sourceURL: "{{ item.url_source }}" - isThirdParty: false - taggingDetails: - deviceRole: "{{ item.device_role }}" - deviceFamilyName: "{{ item.device_family_name }}" + - import_image_details: + type: "{{ item.type }}" + url_details: + payload: "{{ item.url_details.payload }}" + tagging_details: + device_role: "{{ item.device_role }}" + device_family_name: "{{ item.device_family_name }}" + device_type: "{{item.device_type}}" tagging: true - imageDistributionDetails: - deviceSerialNumber: "{{ item.device_serial_number }}" - imageActivationDetails: - scehduleValidate: false - activateLowerImageVersion: true - deviceSerialNumber: "{{ item.device_serial_number }}" - distributeIfNeeded: true + image_distribution_details: + site_name: "{{item.site_name}}" + device_role: "{{ item.device_role }}" + device_family_name: "{{ item.device_family_name }}" + image_activation_details: + site_name: "{{item.site_name}}" + device_role: "{{ item.device_role }}" + device_family_name: "{{ item.device_family_name }}" + scehdule_validate: false + distribute_if_needed: true + with_items: '{{ image_details }}' tags: - swim From a1ec32ca050061d7fd36edccb5fa747dfeeddcc3 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Tue, 19 Dec 2023 16:21:23 +0530 Subject: [PATCH 50/50] remove duplicate key from playbook --- playbooks/test_swim_module.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/playbooks/test_swim_module.yml b/playbooks/test_swim_module.yml index d3ca027f26..7e6e50007a 100644 --- a/playbooks/test_swim_module.yml +++ b/playbooks/test_swim_module.yml @@ -5,9 +5,7 @@ gather_facts: no vars_files: - "input_swim.yml" #Contains image and device details - - "credentials.yml" - gather_facts: false - connection: local + - "credentials.yml" tasks: - name: Import an image, tag it as golden and load it on device cisco.dnac.swim_intent: