From 76c18fa494057102d948d053627bfe5895dcee88 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 18 Oct 2023 16:01:17 +0530 Subject: [PATCH 01/32] Write and test code for adding Network Device in Inventory and creating playbook for the same --- playbooks/inventory_device.yml | 40 ++ playbooks/swim_intent.yml | 38 ++ plugins/modules/inventory_intent.py | 610 ++++++++++++++++++++++++++++ 3 files changed, 688 insertions(+) create mode 100644 playbooks/inventory_device.yml create mode 100644 playbooks/swim_intent.yml create mode 100644 plugins/modules/inventory_intent.py diff --git a/playbooks/inventory_device.yml b/playbooks/inventory_device.yml new file mode 100644 index 0000000000..d489933506 --- /dev/null +++ b/playbooks/inventory_device.yml @@ -0,0 +1,40 @@ +--- +- name: Configure device credentials on Cisco DNA Center + hosts: localhost + connection: local + gather_facts: no + vars_files: + - "input_inventory.yml" + - "credentials.yml" + tasks: + - name: Add/Update/Resync/Delete the devices in DNAC 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: true + state: merged + config: + - enablePassword: "{{item.enablePassword}}" + 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}}" + + with_items: "{{ device_details }}" + tags: + - inventory_device \ No newline at end of file diff --git a/playbooks/swim_intent.yml b/playbooks/swim_intent.yml new file mode 100644 index 0000000000..77a8c217e0 --- /dev/null +++ b/playbooks/swim_intent.yml @@ -0,0 +1,38 @@ +--- +- name: Configure device credentials on Cisco DNA Center + hosts: localhost + connection: local + gather_facts: no + vars_files: + - "input_swim.yml" + - "credentials.yml" + tasks: + - name: Import an image, tag it as golden and load it on device {{ item.device_serial_number }} + 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: + - importImageDetails: + type: "{{ item.type }}" + urlDetails: + payload: "{{ item.urlDetails.payload }}" + taggingDetails: + deviceRole: "{{ item.device_role }}" + deviceFamilyName: "{{ item.device_family_name }}" + tagging: true + imageDistributionDetails: + deviceSerialNumber: "{{ item.device_serial_number }}" + imageActivationDetails: + scehduleValidate: false + activateLowerImageVersion: true + deviceSerialNumber: "{{ item.device_serial_number }}" + distributeIfNeeded: true + with_items: "{{ image_details }}" + tags: + - swim \ No newline at end of file diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py new file mode 100644 index 0000000000..f3a55db2c2 --- /dev/null +++ b/plugins/modules/inventory_intent.py @@ -0,0 +1,610 @@ +#!/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__ = ("Madhan Sankaranarayanan, Abhishek Maheshwari") + +DOCUMENTATION = r""" +--- +module: inventory_intent +short_description: Resource module for Network Device +description: +- Manage operations create, update and delete of the resource Network Device. +- Adds the device with given credential. +- Deletes the network device for the given Id. +- Sync the devices provided as input. +version_added: '3.1.0' +extends_documentation_fragment: + - cisco.dnac.intent_params +author: Abhishek Maheshwari (@abmahesh) +options: + state: + description: The state of DNAC after module completion. + type: str + choices: [ merged, deleted ] + default: merged + config: + description: List of devices with credentails to perform Add/Update/Delete/Resync operation + type: list + elements: dict + required: True + suboptions: + cliTransport: + description: Network Device's cliTransport. + type: str + computeDevice: + description: ComputeDevice flag. + type: bool + enablePassword: + description: Network Device's enablePassword. + type: str + extendedDiscoveryInfo: + description: Network Device's extendedDiscoveryInfo. + type: str + httpPassword: + description: Network Device's httpPassword. + type: str + httpPort: + description: Network Device's httpPort. + type: str + httpSecure: + description: HttpSecure flag. + type: bool + httpUserName: + description: Network Device's httpUserName. + type: str + id: + description: Id path parameter. Device ID. + type: str + ipAddress: + description: Network Device's ipAddress. + elements: str + type: list + merakiOrgId: + description: Network Device's merakiOrgId. + elements: str + type: list + netconfPort: + description: Network Device's netconfPort. + type: str + password: + description: Network Device's password. + type: str + serialNumber: + description: Network Device's serialNumber. + type: str + snmpAuthPassphrase: + description: Network Device's snmpAuthPassphrase. + type: str + snmpAuthProtocol: + description: Network Device's snmpAuthProtocol. + type: str + default: "SHA" + snmpMode: + description: Network Device's snmpMode. + type: str + default: "AUTHPRIV" + snmpPrivPassphrase: + description: Network Device's snmpPrivPassphrase. + type: str + snmpPrivProtocol: + description: Network Device's snmpPrivProtocol. + type: str + default: "AES128" + snmpROCommunity: + description: Network Device's snmpROCommunity. + type: str + default: public + snmpRWCommunity: + description: Network Device's snmpRWCommunity. + type: str + default: private + snmpRetry: + description: Network Device's snmpRetry. + type: int + default: 3 + snmpTimeout: + description: Network Device's snmpTimeout. + type: int + default: 5 + snmpUserName: + description: Network Device's snmpUserName. + type: str + snmpVersion: + description: Network Device's snmpVersion. + type: str + default: "v3" + type: + description: Network Device's type. + type: str + default: "NETWORK_DEVICE" + updateMgmtIPaddressList: + description: Network Device's updateMgmtIPaddressList. + elements: dict + suboptions: + existMgmtIpAddress: + description: Network Device's existMgmtIpAddress. + type: str + newMgmtIpAddress: + description: Network Device's newMgmtIpAddress. + type: str + type: list + userName: + description: Network Device's userName. + type: str + +requirements: +- dnacentersdk >= 2.5.5 +- python >= 3.5 +seealso: +- name: Cisco DNA Center documentation for Devices AddDevice2 + description: Complete reference of the AddDevice2 API. + link: https://developer.cisco.com/docs/dna-center/#!add-device +- name: Cisco DNA Center documentation for Devices DeleteDeviceById + description: Complete reference of the DeleteDeviceById API. + link: https://developer.cisco.com/docs/dna-center/#!delete-device-by-id +- name: Cisco DNA Center documentation for Devices SyncDevices2 + description: Complete reference of the SyncDevices2 API. + link: https://developer.cisco.com/docs/dna-center/#!sync-devices +notes: + - SDK Method used are + devices.Devices.add_device, + devices.Devices.delete_device_by_id, + devices.Devices.sync_devices, + + - Paths used are + post /dna/intent/api/v1/network-device, + delete /dna/intent/api/v1/network-device/{id}, + put /dna/intent/api/v1/network-device, + + - Removed 'managementIpAddress' options in v4.3.0. +""" + +EXAMPLES = r""" +- name: Add new device in Inventory with full credentials + 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: + - cliTransport: string + computeDevice: true + enablePassword: string + extendedDiscoveryInfo: string + httpPassword: string + httpPort: string + httpSecure: true + httpUserName: string + ipAddress: + - string + merakiOrgId: + - string + netconfPort: 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 + type: string + updateMgmtIPaddressList: + - existMgmtIpAddress: string + newMgmtIpAddress: string + userName: string + +- name: Delete Device by id + 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: + - cleanConfig: false + id: string + +""" + +RETURN = r""" + +dnac_response: + description: A dictionary or list with the response returned by the Cisco DNAC Python SDK + returned: always + type: dict + sample: > + { + "response": { + "taskId": "string", + "url": "string" + }, + "version": "string" + } +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( + DnacBase, + validate_list_of_dicts, + log, +) + + +class DnacDevice(DnacBase): + """Class containing member attributes for site intent module""" + + def __init__(self, module): + super().__init__(module) + self.supported_states = ["merged", "deleted"] + + def validate_input(self): + """Validate the fields provided in the playbook""" + + if not self.config: + self.msg = "config not available in playbook for validation" + self.status = "failed" + return self + + 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'}, + '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'}, + 'resync': {'type': 'bool'} + } + + # Validate site params + valid_temp, invalid_params = validate_list_of_dicts( + self.config, temp_spec + ) + + if invalid_params: + self.msg = "Invalid parameters in playbook: {0}".format( + "\n".join(invalid_params) + ) + self.status = "failed" + return self + + self.validated_config = valid_temp + self.log(str(valid_temp)) + self.msg = "Successfully validated input" + self.status = "success" + return self + + def device_exists_in_dnac(self, want_device): + """Check which devices already exists in DNAC and return both device_exist and device_not_exist in dnac """ + + device_in_dnac = [] + response = None + + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + ) + + except Exception as e: + self.log("There is error while fetching device from DNAC") + + if response: + self.log(str(response)) + response = response.get("response") + for ip in response: + device_ip = ip["managementIpAddress"] + device_in_dnac.append(device_ip) + + return device_in_dnac + + def get_device_id(self, device_ip): + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"managementIpAddress": device_ip}, + ) + + except Exception as e: + self.log("There is error while fetching device from DNAC") + response = response.get("response")[0] + device_id = response.get("id") + + return device_id + + def get_execution_details(self, task_id): + + response = None + response = self.dnac._exec( + family="task", + function='get_task_by_id', + params={"task_id": task_id} + ) + + self.log(str(response)) + + if response and isinstance(response, dict): + return response.get('response') + + def mandatory_parameter(self, config): + + try: + device_type = config.type + except Exception as e: + device_type = "NETWORK_DEVICE" + + mandatory_params_absent = [] + if device_type == "NETWORK_DEVICE": + params_list = ["enablePassword", "ipAddress", "password", "snmpAuthPassphrase", "snmpPrivPassphrase", "snmpUserName", "userName"] + + for param in params_list: + if param not in config: + mandatory_params_absent.append(param) + + if mandatory_params_absent: + self.msg = "Mandatory paramters {0} not present".format(mandatory_params_absent) + self.result['msg'] = "Required parameters {0} for adding devices are not present".format(mandatory_params_absent) + self.status = "failed" + self.module.fail_json(msg=self.msg, response=self.result) + + return config + + def get_have(self, config): + """Get the device details from DNAC""" + + have = {} + want_device = config.get("ipAddress") + + # Get the list of device that are present in DNAC + device_in_dnac = self.device_exists_in_dnac(want_device) + device_not_in_dnac = [] + + for ip in want_device: + if ip not in device_in_dnac: + device_not_in_dnac.append(ip) + + self.log("Device Exists in DNAC : " + str(device_in_dnac)) + have["want_device"] = want_device + have["device_in_dnac"] = device_in_dnac + have["device_not_in_dnac"] = device_not_in_dnac + + self.have = have + + return self + + def get_device_params(self, params): + """Store device parameters from the playbook for device Add/Update/Edit/Delete processing in DNAC""" + + device_param = { + "enable_password": params.get("enablePassword"), + "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"), + "type": params.get("type"), + "update_management_ip_list": params.get("updateMgmtIPaddressList") + } + + return device_param + + def get_want(self, config): + """Get all the device related information from playbook + that is needed to be add/update/delete/resync device in DNAC""" + + want = {} + device_params = self.get_device_params(config) + want["device_params"] = device_params + + self.want = want + self.msg = "Successfully collected all parameters from playbook " + \ + "for comparison" + self.status = "success" + return self + + def get_diff_merged(self, config): + """Update/Create site info in DNAC with fields provided in DNAC""" + + device_added = False + device_updated = False + device_resynced = False + + devices_to_add = self.have["device_not_in_dnac"] + self.result['msg'] = [] + + if not devices_to_add: + # Write code for device updation + device_updated = True + + self.log("Devices {0} are present in DNAC and updated successfully".format(config['ipAddress'])) + msg = "Devices {0} present in DNAC and updated successfully".format(config['ipAddress']) + self.result['msg'].append(msg) + self.status = "success" + return self + + else: + # If we want to add device in inventory + config = self.mandatory_parameter(config) + config['ipAddress'] = devices_to_add + del config['resync'] + + try: + response = self.dnac._exec( + family="devices", + function='add_device', + op_modifies=True, + params=config, + ) + + self.log(str(response)) + device_added = True + + except Exception as e: + self.log("There is error while adding devices in DNAC") + + if device_added or device_updated: + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + while True: + execution_details = self.get_execution_details(task_id) + + if '/task/' in execution_details.get("progress"): + self.result['changed'] = True + self.result['response'] = execution_details + break + + elif execution_details.get("isError") and execution_details.get("failureReason"): + self.module.fail_json(msg=execution_details.get("failureReason"), + response=execution_details) + break + + self.log("Device Added Successfully") + self.log("Added devices are :" + str(devices_to_add)) + msg = "Device " + str(devices_to_add) + " added Successfully !!" + self.result['msg'].append(msg) + + return self + + def get_diff_deleted(self, config): + """Call DNAC API to delete devices with provided inputs""" + + device_to_delete = config.get("ipAddress") + self.result['msg'] = [] + + for device_ip in device_to_delete: + if device_ip in self.have.get("device_in_dnac"): + device_id = self.get_device_id(device_ip) + try: + response = self.dnac._exec( + family="devices", + function='delete_device_by_id', + params={"id": device_id}, + ) + + except Exception as e: + self.log("There is error while deleting the device from DNAC") + + if response and isinstance(response, dict): + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + while True: + execution_details = self.get_execution_details(task_id) + + if 'success' in execution_details.get("progress"): + self.result['changed'] = True + self.result['response'] = execution_details + break + + elif execution_details.get("isError") and execution_details.get("failureReason"): + self.module.fail_json(msg=execution_details.get("failureReason"), + response=execution_details) + break + + else: + self.result['changed'] = False + msg = "The device {0} is not present in DNAC so can't perform delete operation".format(device_ip) + self.result['msg'].append(msg) + + 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_device = DnacDevice(module) + state = dnac_device.params.get("state") + + if state not in dnac_device.supported_states: + dnac_device.status = "invalid" + dnac_device.msg = "State {0} is invalid".format(state) + dnac_device.check_return_status() + + dnac_device.validate_input().check_return_status() + + for config in dnac_device.validated_config: + dnac_device.reset_values() + dnac_device.get_want(config).check_return_status() + dnac_device.get_have(config).check_return_status() + dnac_device.get_diff_state_apply[state](config).check_return_status() + + module.exit_json(**dnac_device.result) + + +if __name__ == '__main__': + main() From e39e9a106a8cc93ee79122927e1378827f98d400 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 18 Oct 2023 16:30:03 +0530 Subject: [PATCH 02/32] renamed get_execution_details function to get_task_details --- plugins/modules/inventory_intent.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index f3a55db2c2..027eb49d8f 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -353,7 +353,7 @@ def get_device_id(self, device_ip): return device_id - def get_execution_details(self, task_id): + def get_task_details(self, task_id): response = None response = self.dnac._exec( @@ -505,7 +505,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_execution_details(task_id) + execution_details = self.get_task_details(task_id) if '/task/' in execution_details.get("progress"): self.result['changed'] = True @@ -547,7 +547,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_execution_details(task_id) + execution_details = self.get_task_details(task_id) if 'success' in execution_details.get("progress"): self.result['changed'] = True From 1715f5f56f20ad70e74b8da44f255f0e2d91998e Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Wed, 18 Oct 2023 16:37:46 +0530 Subject: [PATCH 03/32] changes self.log to log --- plugins/modules/inventory_intent.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 027eb49d8f..bec31af3d4 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -253,7 +253,7 @@ class DnacDevice(DnacBase): - """Class containing member attributes for site intent module""" + """Class containing member attributes for Inventory intent module""" def __init__(self, module): super().__init__(module) @@ -309,7 +309,7 @@ def validate_input(self): return self self.validated_config = valid_temp - self.log(str(valid_temp)) + log(str(valid_temp)) self.msg = "Successfully validated input" self.status = "success" return self @@ -327,10 +327,10 @@ def device_exists_in_dnac(self, want_device): ) except Exception as e: - self.log("There is error while fetching device from DNAC") + log("There is error while fetching device from DNAC") if response: - self.log(str(response)) + log(str(response)) response = response.get("response") for ip in response: device_ip = ip["managementIpAddress"] @@ -347,7 +347,7 @@ def get_device_id(self, device_ip): ) except Exception as e: - self.log("There is error while fetching device from DNAC") + log("There is error while fetching device from DNAC") response = response.get("response")[0] device_id = response.get("id") @@ -362,7 +362,7 @@ def get_task_details(self, task_id): params={"task_id": task_id} ) - self.log(str(response)) + log(str(response)) if response and isinstance(response, dict): return response.get('response') @@ -404,7 +404,7 @@ def get_have(self, config): if ip not in device_in_dnac: device_not_in_dnac.append(ip) - self.log("Device Exists in DNAC : " + str(device_in_dnac)) + log("Device Exists in DNAC : " + str(device_in_dnac)) have["want_device"] = want_device have["device_in_dnac"] = device_in_dnac have["device_not_in_dnac"] = device_not_in_dnac @@ -475,7 +475,7 @@ def get_diff_merged(self, config): # Write code for device updation device_updated = True - self.log("Devices {0} are present in DNAC and updated successfully".format(config['ipAddress'])) + log("Devices {0} are present in DNAC and updated successfully".format(config['ipAddress'])) msg = "Devices {0} present in DNAC and updated successfully".format(config['ipAddress']) self.result['msg'].append(msg) self.status = "success" @@ -495,11 +495,11 @@ def get_diff_merged(self, config): params=config, ) - self.log(str(response)) + log(str(response)) device_added = True except Exception as e: - self.log("There is error while adding devices in DNAC") + log("There is error while adding devices in DNAC") if device_added or device_updated: if response and isinstance(response, dict): @@ -517,8 +517,8 @@ def get_diff_merged(self, config): response=execution_details) break - self.log("Device Added Successfully") - self.log("Added devices are :" + str(devices_to_add)) + log("Device Added Successfully") + log("Added devices are :" + str(devices_to_add)) msg = "Device " + str(devices_to_add) + " added Successfully !!" self.result['msg'].append(msg) @@ -541,7 +541,7 @@ def get_diff_deleted(self, config): ) except Exception as e: - self.log("There is error while deleting the device from DNAC") + log("There is error while deleting the device from DNAC") if response and isinstance(response, dict): if response and isinstance(response, dict): From e24f6417f7016546f40d126f327a7975c4a4c1b8 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 19 Oct 2023 12:04:49 +0530 Subject: [PATCH 04/32] add docstring for all the api for create/delete device in Inventory --- plugins/modules/inventory_intent.py | 99 ++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 10 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index bec31af3d4..8f8ccdda4a 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -315,7 +315,15 @@ def validate_input(self): return self def device_exists_in_dnac(self, want_device): - """Check which devices already exists in DNAC and return both device_exist and device_not_exist in dnac """ + """ + Check which devices already exists in DNAC 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 DNAC. + Returns: + list: A list of devices that exist in DNAC. + Description: + If a device is found in DNAC, its management IP address is included in the list of devices that exist. + """ device_in_dnac = [] response = None @@ -339,21 +347,40 @@ def device_exists_in_dnac(self, want_device): return device_in_dnac def get_device_id(self, device_ip): + """ + Get the unique device ID for a device with the specified management IP address in Cisco DNAC. + Args: + device_ip (str): The management IP address of the device for which you want to retrieve the device ID. + Returns: + str: The unique device ID for the specified device. + Description: + If the device with the specified IP address is not found in DNAC, this function will raise an exception. + """ try: response = self.dnac._exec( family="devices", function='get_device_list', params={"managementIpAddress": device_ip}, ) - except Exception as e: log("There is error while fetching device from DNAC") + response = response.get("response")[0] device_id = response.get("id") return device_id def get_task_details(self, task_id): + """ + Get the details of a specific task in Cisco DNAC. + Args: + task_id (str): The unique identifier of the task for which you want to retrieve details. + Returns: + dict or None: A dictionary containing detailed information about the specified task, + or None if the task with the given task_id is not found. + Description: + If the task with the specified task ID is not found in DNAC, this function will return None. + """ response = None response = self.dnac._exec( @@ -368,6 +395,15 @@ def get_task_details(self, task_id): return response.get('response') def mandatory_parameter(self, config): + """ + Check for and validate mandatory parameters for adding network devices in Cisco DNAC. + Args: + config (dict): A dictionary containing the configuration details for adding a network device to DNAC. + Returns: + dict: The input `config` dictionary if all mandatory parameters are present. + Description: + It will check the mandatory parameters for adding the devices in DNAC and if any parameter is missing it wil raise AnsibleModuleError. + """ try: device_type = config.type @@ -391,7 +427,18 @@ def mandatory_parameter(self, config): return config def get_have(self, config): - """Get the device details from DNAC""" + """ + Retrieve and check device information with DNAC to determine if devices already exist. + Args: + config (dict): A dictionary containing the configuration details of devices to be checked. + Returns: + dict: A dictionary containing information about the devices in the playbook, devices that exist in DNAC, and devices that are not present in DNAC. + Description: + This function will check resulting `have` dictionary will contain the following keys: + - "want_device": A list of devices specified in the playbook. + - "device_in_dnac": A list of devices that already exist in DNAC. + - "device_not_in_dnac": A list of devices that are not present in DNAC. + """ have = {} want_device = config.get("ipAddress") @@ -414,7 +461,15 @@ def get_have(self, config): return self def get_device_params(self, params): - """Store device parameters from the playbook for device Add/Update/Edit/Delete processing in DNAC""" + """ + Extract and store device parameters from the playbook for device processing in DNAC. + Args: + params (dict): A dictionary containing device parameters retrieved from the playbook. + Returns: + dict: A dictionary containing the extracted device parameters. + Description: + This function will extract and store parameters in dictionary for adding, updating, editing, or deleting devices DNAC. + """ device_param = { "enable_password": params.get("enablePassword"), @@ -448,8 +503,15 @@ def get_device_params(self, params): return device_param def get_want(self, config): - """Get all the device related information from playbook - that is needed to be add/update/delete/resync device in DNAC""" + """ + Get all the device related information from playbook that is needed to be add/update/delete/resync device in DNAC + Args: + config (dict): A dictionary containing device-related information from the playbook. + Returns: + dict: A dictionary containing the extracted device parameters and other relevant information. + Description: + This function typically retrieve device-related information from the playbook for device operations in DNAC. + """ want = {} device_params = self.get_device_params(config) @@ -459,14 +521,24 @@ def get_want(self, config): self.msg = "Successfully collected all parameters from playbook " + \ "for comparison" self.status = "success" + return self def get_diff_merged(self, config): - """Update/Create site info in DNAC with fields provided in DNAC""" + """ + Merge and process differences between existing devices and desired device configuration in DNAC. + Args: + config (dict): A dictionary containing the desired device configuration and relevant information from the playbook. + Returns: + object: An instance of the class with updated results and status based on the processing of differences. + Description: + The function processes the differences and, depending on the changes required, it may add, update, or resynchronize devices in DNAC. + The updated results and status are stored in the class instance for further use. + """ device_added = False device_updated = False - device_resynced = False + # device_resynced = False devices_to_add = self.have["device_not_in_dnac"] self.result['msg'] = [] @@ -525,7 +597,15 @@ def get_diff_merged(self, config): return self def get_diff_deleted(self, config): - """Call DNAC API to delete devices with provided inputs""" + """ + Delete devices in DNAC based on provided inputs. + Args: + 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 DNAC inventory. + """ device_to_delete = config.get("ipAddress") self.result['msg'] = [] @@ -539,7 +619,6 @@ def get_diff_deleted(self, config): function='delete_device_by_id', params={"id": device_id}, ) - except Exception as e: log("There is error while deleting the device from DNAC") From 4a8705ebb5fd72c1effe8747d415c39bf251d008 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 20 Oct 2023 10:43:59 +0530 Subject: [PATCH 05/32] Address PR Review comments and optimised the code and handle exception --- plugins/module_utils/dnac.py | 30 ++- plugins/modules/inventory_intent.py | 299 ++++++++++++++-------------- 2 files changed, 175 insertions(+), 154 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 568c58efc5..b5f1ab1d46 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -60,7 +60,10 @@ def __init__(self, module): @abstractmethod def validate_input(self): - pass + if not self.config: + self.msg = "config not available in playbook for validation" + self.status = "failed" + return self def get_diff_merged(self): # Implement logic to merge the resource configuration @@ -129,18 +132,29 @@ def get_dnac_params(self, params): return dnac_params def get_task_details(self, task_id): - """Check if the task performed is sucessfull or not""" + """ + Get the details of a specific task in Cisco DNA Center. + Args: + self (object): An instance of a class that provides access to Cisco DNA Center. + task_id (str): The unique identifier of the task for which you want to retrieve details. + Returns: + dict or None: A dictionary containing detailed information about the specified task, + or None if the task with the given task_id is not found. + Description: + If the task with the specified task ID is not found in Cisco DNA Center, this function will return None. + """ result = None - response = self.dnac_apply['exec']( + response = self.dnac._exec( family="task", - function="get_task_by_id", - params={"task_id": task_id}, + function='get_task_by_id', + params={"task_id": task_id} ) - self.log(str(response)) - if isinstance(response, dict): - result = response.get("response") + log(str(response)) + + if response and isinstance(response, dict): + result = response.get('response') return result diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 8f8ccdda4a..a84dea7bc3 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -24,7 +24,7 @@ author: Abhishek Maheshwari (@abmahesh) options: state: - description: The state of DNAC after module completion. + description: The state of Cisco DNA Center after module completion. type: str choices: [ merged, deleted ] default: merged @@ -35,13 +35,13 @@ required: True suboptions: cliTransport: - description: Network Device's cliTransport. + description: Network Device's cliTransport.Required for Adding Network Device. type: str computeDevice: description: ComputeDevice flag. type: bool enablePassword: - description: Network Device's enablePassword. + description: Network Device's enablePassword.Required for Adding Network Device. type: str extendedDiscoveryInfo: description: Network Device's extendedDiscoveryInfo. @@ -59,10 +59,10 @@ description: Network Device's httpUserName. type: str id: - description: Id path parameter. Device ID. + description: Id path parameter. Device ID.Required for Deleting Device. type: str ipAddress: - description: Network Device's ipAddress. + description: Network Device's ipAddress.Required for Adding/Deleting Device. elements: str type: list merakiOrgId: @@ -73,47 +73,47 @@ description: Network Device's netconfPort. type: str password: - description: Network Device's password. + description: Network Device's password.Required for Adding Network Device. type: str serialNumber: description: Network Device's serialNumber. type: str snmpAuthPassphrase: - description: Network Device's snmpAuthPassphrase. + description: Network Device's snmpAuthPassphrase.Required for Adding Network Device. type: str snmpAuthProtocol: - description: Network Device's snmpAuthProtocol. + description: Network Device's snmpAuthProtocol.Required for Adding Network Device. type: str default: "SHA" snmpMode: - description: Network Device's snmpMode. + description: Network Device's snmpMode.Required for Adding Network Device. type: str default: "AUTHPRIV" snmpPrivPassphrase: - description: Network Device's snmpPrivPassphrase. + description: Network Device's snmpPrivPassphrase.Required for Adding Network Device. type: str snmpPrivProtocol: - description: Network Device's snmpPrivProtocol. + description: Network Device's snmpPrivProtocol.Required for Adding Network Device. type: str default: "AES128" snmpROCommunity: - description: Network Device's snmpROCommunity. + description: Network Device's snmpROCommunity.Required for Adding V2C Network Device. type: str default: public snmpRWCommunity: - description: Network Device's snmpRWCommunity. + description: Network Device's snmpRWCommunity.Required for Adding V2C Network Device. type: str default: private snmpRetry: - description: Network Device's snmpRetry. + description: Network Device's snmpRetry.Required for Adding Network Device. type: int default: 3 snmpTimeout: - description: Network Device's snmpTimeout. + description: Network Device's snmpTimeout.Required for Adding Network Device. type: int default: 5 snmpUserName: - description: Network Device's snmpUserName. + description: Network Device's snmpUserName.Required for Adding Network Device. type: str snmpVersion: description: Network Device's snmpVersion. @@ -135,7 +135,7 @@ type: str type: list userName: - description: Network Device's userName. + description: Network Device's userName.Required for Adding Network Device. type: str requirements: @@ -230,7 +230,7 @@ RETURN = r""" dnac_response: - description: A dictionary or list with the response returned by the Cisco DNAC Python SDK + description: A dictionary or list with the response returned by the Cisco DNA Center Python SDK returned: always type: dict sample: > @@ -260,12 +260,23 @@ def __init__(self, module): self.supported_states = ["merged", "deleted"] def validate_input(self): - """Validate the fields provided in the playbook""" - - if not self.config: - self.msg = "config not available in playbook for validation" - self.status = "failed" - return 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. + """ temp_spec = {'cliTransport': {'default': "telnet", 'type': 'str'}, 'computeDevice': {'type': 'bool'}, @@ -296,7 +307,7 @@ def validate_input(self): 'resync': {'type': 'bool'} } - # Validate site params + # Validate device params valid_temp, invalid_params = validate_list_of_dicts( self.config, temp_spec ) @@ -316,17 +327,21 @@ def validate_input(self): def device_exists_in_dnac(self, want_device): """ - Check which devices already exists in DNAC and return both device_exist and device_not_exist in dnac. + 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 DNAC. + want_device (list): A list of devices you want to check for existence in Cisco DNA Center. Returns: - list: A list of devices that exist in DNAC. + list: A list of devices that exist in Cisco DNA Center. Description: - If a device is found in DNAC, its management IP address is included in the list of devices that exist. + 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. + 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. """ device_in_dnac = [] - response = None try: response = self.dnac._exec( @@ -335,7 +350,8 @@ def device_exists_in_dnac(self, want_device): ) except Exception as e: - log("There is error while fetching device from DNAC") + log("An error occurred while fetching the device from Cisco DNA Center") + raise Exception("Error while fetching device from Cisco DNA Center") if response: log(str(response)) @@ -348,13 +364,14 @@ def device_exists_in_dnac(self, want_device): def get_device_id(self, device_ip): """ - Get the unique device ID for a device with the specified management IP address in Cisco DNAC. + Get the unique device ID for a device with the specified management IP address in Cisco DNA Center. Args: device_ip (str): The management IP address of the device for which you want to retrieve the device ID. Returns: str: The unique device ID for the specified device. Description: - If the device with the specified IP address is not found in DNAC, this function will raise an exception. + Queries Cisco DNA Center to retrieve the unique device ID associated with a device having the specified + IP address. If the device is not found in Cisco DNA Center, it raises an exception. """ try: response = self.dnac._exec( @@ -363,56 +380,30 @@ def get_device_id(self, device_ip): params={"managementIpAddress": device_ip}, ) except Exception as e: - log("There is error while fetching device from DNAC") + log("An error occurred while fetching the device from Cisco DNA Center") + raise Exception("Error while fetching device from Cisco DNA Center") response = response.get("response")[0] device_id = response.get("id") return device_id - def get_task_details(self, task_id): - """ - Get the details of a specific task in Cisco DNAC. - Args: - task_id (str): The unique identifier of the task for which you want to retrieve details. - Returns: - dict or None: A dictionary containing detailed information about the specified task, - or None if the task with the given task_id is not found. - Description: - If the task with the specified task ID is not found in DNAC, this function will return None. - """ - - response = None - response = self.dnac._exec( - family="task", - function='get_task_by_id', - params={"task_id": task_id} - ) - - log(str(response)) - - if response and isinstance(response, dict): - return response.get('response') - def mandatory_parameter(self, config): """ - Check for and validate mandatory parameters for adding network devices in Cisco DNAC. + 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 DNAC. + config (dict): A dictionary containing the configuration details for adding a network device to Cisco DNA Center. Returns: dict: The input `config` dictionary if all mandatory parameters are present. Description: - It will check the mandatory parameters for adding the devices in DNAC and if any parameter is missing it wil raise AnsibleModuleError. + It will check the mandatory parameters for adding the devices in Cisco DNA Center. """ - try: - device_type = config.type - except Exception as e: - device_type = "NETWORK_DEVICE" + device_type = config.get("type", "NETWORK_DEVICE") mandatory_params_absent = [] if device_type == "NETWORK_DEVICE": - params_list = ["enablePassword", "ipAddress", "password", "snmpAuthPassphrase", "snmpPrivPassphrase", "snmpUserName", "userName"] + params_list = ["enablePassword", "ipAddress", "password", "snmpUserName", "snmpAuthPassphrase", "snmpPrivPassphrase", "userName"] for param in params_list: if param not in config: @@ -422,28 +413,33 @@ def mandatory_parameter(self, config): self.msg = "Mandatory paramters {0} not present".format(mandatory_params_absent) self.result['msg'] = "Required parameters {0} for adding devices are not present".format(mandatory_params_absent) self.status = "failed" - self.module.fail_json(msg=self.msg, response=self.result) + 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." + self.status = "success" - return config + return self def get_have(self, config): """ - Retrieve and check device information with DNAC to determine if devices already exist. + Retrieve and check device information with Cisco DNA Center to determine if devices already exist. Args: + 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: - dict: A dictionary containing information about the devices in the playbook, devices that exist in DNAC, and devices that are not present in DNAC. + dict: A dictionary containing information about the devices in the playbook, devices that exist in + Cisco DNA Center, and devices that are not present in Cisco DNA Center. Description: - This function will check resulting `have` dictionary will contain the following keys: + This function checks the specified devices in the playbook against the devices existing in Cisco DNA Center with following keys: - "want_device": A list of devices specified in the playbook. - - "device_in_dnac": A list of devices that already exist in DNAC. - - "device_not_in_dnac": A list of devices that are not present in DNAC. + - "device_in_dnac": A list of devices that already exist in Cisco DNA Center. + - "device_not_in_dnac": A list of devices that are not present in Cisco DNA Center. """ have = {} want_device = config.get("ipAddress") - # Get the list of device that are present in DNAC + # Get the list of device that are present in Cisco DNA Center device_in_dnac = self.device_exists_in_dnac(want_device) device_not_in_dnac = [] @@ -451,7 +447,7 @@ def get_have(self, config): if ip not in device_in_dnac: device_not_in_dnac.append(ip) - log("Device Exists in DNAC : " + str(device_in_dnac)) + log("Device Exists in Cisco DNA Center : " + str(device_in_dnac)) have["want_device"] = want_device have["device_in_dnac"] = device_in_dnac have["device_not_in_dnac"] = device_not_in_dnac @@ -462,13 +458,14 @@ def get_have(self, config): def get_device_params(self, params): """ - Extract and store device parameters from the playbook for device processing in DNAC. + Extract and store device parameters from the playbook for device processing in Cisco DNA Center. Args: + 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: dict: A dictionary containing the extracted device parameters. Description: - This function will extract and store parameters in dictionary for adding, updating, editing, or deleting devices DNAC. + This function will extract and store parameters in dictionary for adding, updating, editing, or deleting devices Cisco DNA Center. """ device_param = { @@ -504,13 +501,16 @@ def get_device_params(self, params): def get_want(self, config): """ - Get all the device related information from playbook that is needed to be add/update/delete/resync device in DNAC + Get all the device related information from playbook that is needed to be + add/update/delete/resync device in Cisco DNA Center. Args: + 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: dict: A dictionary containing the extracted device parameters and other relevant information. Description: - This function typically retrieve device-related information from the playbook for device operations in DNAC. + Retrieve all the device-related information from the playbook needed for adding, updating, deleting, + or resyncing devices in Cisco DNA Center. """ want = {} @@ -518,21 +518,22 @@ def get_want(self, config): want["device_params"] = device_params self.want = want - self.msg = "Successfully collected all parameters from playbook " + \ - "for comparison" + self.msg = "Successfully collected all parameters from the playbook " self.status = "success" return self def get_diff_merged(self, config): """ - Merge and process differences between existing devices and desired device configuration in DNAC. + Merge and process differences between existing devices and desired device configuration in Cisco DNA Center. Args: + 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: 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 DNAC. + 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. """ @@ -541,107 +542,113 @@ def get_diff_merged(self, config): # device_resynced = False devices_to_add = self.have["device_not_in_dnac"] - self.result['msg'] = [] + self.result['log'] = [] if not devices_to_add: # Write code for device updation device_updated = True - log("Devices {0} are present in DNAC and updated successfully".format(config['ipAddress'])) - msg = "Devices {0} present in DNAC and updated successfully".format(config['ipAddress']) - self.result['msg'].append(msg) + 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 - else: - # If we want to add device in inventory - config = self.mandatory_parameter(config) - config['ipAddress'] = devices_to_add - del config['resync'] + # If we want to add device in inventory + self.mandatory_parameter(config).check_return_status() + config['ipAddress'] = devices_to_add + del config['resync'] - try: - response = self.dnac._exec( - family="devices", - function='add_device', - op_modifies=True, - params=config, - ) + try: + response = self.dnac._exec( + family="devices", + function='add_device', + op_modifies=True, + params=config, + ) - log(str(response)) - device_added = True + log(str(response)) + device_added = True - except Exception as e: - log("There is error while adding devices in DNAC") - - if device_added or device_updated: - 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.result['changed'] = True - self.result['response'] = execution_details - break - - elif execution_details.get("isError") and execution_details.get("failureReason"): - self.module.fail_json(msg=execution_details.get("failureReason"), - response=execution_details) - break - - log("Device Added Successfully") - log("Added devices are :" + str(devices_to_add)) - msg = "Device " + str(devices_to_add) + " added Successfully !!" - self.result['msg'].append(msg) + if device_added or device_updated: + 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['changed'] = True + self.result['response'] = execution_details + 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.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: + log("An error occurred while Adding device in Cisco DNA Center") + raise Exception("Error while Adding device in Cisco DNA Center") return self def get_diff_deleted(self, config): """ - Delete devices in DNAC based on provided inputs. + Delete devices in Cisco DNA Center based on device IP Address. Args: + 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 DNAC inventory. + This function is responsible for removing devices from the Cisco DNA Center inventory and + raise Exception if any error occured. """ device_to_delete = config.get("ipAddress") self.result['msg'] = [] for device_ip in device_to_delete: - if device_ip in self.have.get("device_in_dnac"): - device_id = self.get_device_id(device_ip) - try: - response = self.dnac._exec( - family="devices", - function='delete_device_by_id', - params={"id": device_id}, - ) - except Exception as e: - log("There is error while deleting the device from DNAC") + 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.status = "success" + self.result['msg'].append(msg) + continue + + device_id = self.get_device_id(device_ip) + try: + response = self.dnac._exec( + family="devices", + function='delete_device_by_id', + params={"id": device_id}, + ) if response and isinstance(response, dict): - if response and isinstance(response, dict): - task_id = response.get('response').get('taskId') + 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 = "Device Deleted Successfully from Cisco DNA Center" + self.status = "success" self.result['changed'] = True self.result['response'] = execution_details break - elif execution_details.get("isError") and execution_details.get("failureReason"): - self.module.fail_json(msg=execution_details.get("failureReason"), - response=execution_details) + self.msg = "Device Deletion get failed because of {0}".format(execution_details.get("failureReason")) + self.status = "failed" break - else: - self.result['changed'] = False - msg = "The device {0} is not present in DNAC so can't perform delete operation".format(device_ip) - self.result['msg'].append(msg) + except Exception as e: + log("An error occurred while Deleting the device from Cisco DNA Center") + raise Exception("Error while Deleting device from Cisco DNA Center") return self @@ -650,7 +657,7 @@ def main(): """ main entry point for module execution """ - element_spec = {'dnac_host': {'required': True, 'type': 'str'}, + element_spec = {'dnac_host': {'type': 'str', 'required': True, }, 'dnac_port': {'type': 'str', 'default': '443'}, 'dnac_username': {'type': 'str', 'default': 'admin', 'aliases': ['user']}, 'dnac_password': {'type': 'str', 'no_log': True}, From 279facc11907f739cf374ab3134a384a4445670a Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 26 Oct 2023 15:13:21 +0530 Subject: [PATCH 06/32] Add other device types(Compute,Meraki,Firepower,Third Party Devices) and add examples in documentation --- plugins/modules/inventory_intent.py | 124 +++++++++++++++++++++------- 1 file changed, 94 insertions(+), 30 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index a84dea7bc3..f599449222 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -35,34 +35,35 @@ required: True suboptions: cliTransport: - description: Network Device's cliTransport.Required for Adding Network Device. + description: Network Device's cliTransport.Required for Adding Network Devices. type: str computeDevice: description: ComputeDevice flag. type: bool enablePassword: - description: Network Device's enablePassword.Required for Adding Network Device. + description: Network Device's enablePassword. type: str extendedDiscoveryInfo: description: Network Device's extendedDiscoveryInfo. type: str httpPassword: - description: Network Device's httpPassword. + description: Network Device's httpPassword.Required for Adding Compute, Meraki, + Firepower Management System Devices. type: str httpPort: - description: Network Device's httpPort. + description: Network Device's httpPort.Required for Adding Compute, Firepower Management System Devices. type: str httpSecure: description: HttpSecure flag. type: bool httpUserName: - description: Network Device's httpUserName. + description: Network Device's httpUserName.Required for Adding Compute,Firepower Management System Devices. type: str id: description: Id path parameter. Device ID.Required for Deleting Device. type: str ipAddress: - description: Network Device's ipAddress.Required for Adding/Deleting Device. + description: Network Device's ipAddress.Required for Adding/Deleting Device except Meraki Devices. elements: str type: list merakiOrgId: @@ -79,41 +80,41 @@ description: Network Device's serialNumber. type: str snmpAuthPassphrase: - description: Network Device's snmpAuthPassphrase.Required for Adding Network Device. + description: Network Device's snmpAuthPassphrase.Required for Adding Network, Compute, Third Party Devices. type: str snmpAuthProtocol: - description: Network Device's snmpAuthProtocol.Required for Adding Network Device. + description: Network Device's snmpAuthProtocol. type: str default: "SHA" snmpMode: - description: Network Device's snmpMode.Required for Adding Network Device. + description: Network Device's snmpMode. type: str default: "AUTHPRIV" snmpPrivPassphrase: - description: Network Device's snmpPrivPassphrase.Required for Adding Network Device. + description: Network Device's snmpPrivPassphrase.Required for Adding Network, Compute, Third Party Devices. type: str snmpPrivProtocol: - description: Network Device's snmpPrivProtocol.Required for Adding Network Device. + description: Network Device's snmpPrivProtocol.Required for Adding Network, Compute, Third Party Devices. type: str default: "AES128" snmpROCommunity: - description: Network Device's snmpROCommunity.Required for Adding V2C Network Device. + description: Network Device's snmpROCommunity.Required for Adding V2C Devices. type: str default: public snmpRWCommunity: - description: Network Device's snmpRWCommunity.Required for Adding V2C Network Device. + description: Network Device's snmpRWCommunity.Required for Adding V2C Devices. type: str default: private snmpRetry: - description: Network Device's snmpRetry.Required for Adding Network Device. + description: Network Device's snmpRetry. type: int default: 3 snmpTimeout: - description: Network Device's snmpTimeout.Required for Adding Network Device. + description: Network Device's snmpTimeout. type: int default: 5 snmpUserName: - description: Network Device's snmpUserName.Required for Adding Network Device. + description: Network Device's snmpUserName.Required for Adding Network, Compute, Third Party Devices. type: str snmpVersion: description: Network Device's snmpVersion. @@ -210,6 +211,53 @@ newMgmtIpAddress: string userName: string +- name: Add new Compute device in Inventory with full credentials.Inputs needed for Compute Device + 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 + resync: false + type: "COMPUTE_DEVICE" + +- name: Add new Meraki device in Inventory with full credentials.Inputs needed for Meraki Device + config: + - httpPassword: string + resync: false + type: "MERAKI_DASHBOARD" + +- name: Add new Firepower Management System device in Inventory with full credentials.Input needed for Firepower Management System Device + config: + - ipAddress: string + httpUserName: string + httpPassword: string + httpPort: string + resync: false + type: "FIREPOWER_MANAGEMENT_SYSTEM" + +- name: Add new Third Party device in Inventory with full credentials.Input needed for Third Party Device + config: + - ipAddress: string + snmpAuthPassphrase: string + snmpAuthProtocol: string + snmpMode: string + snmpPrivPassphrase: string + snmpPrivProtocol: string + snmpRetry: 3 + snmpTimeout: 5 + snmpUserName: string + resync: false + type: "THIRD_PARTY_DEVICE" + - name: Delete Device by id cisco.dnac.inventory_intent: dnac_host: "{{dnac_host}}" @@ -399,24 +447,36 @@ def mandatory_parameter(self, config): It will check the mandatory parameters for adding the devices in Cisco DNA Center. """ - device_type = config.get("type", "NETWORK_DEVICE") + device_type = self.config[0].get("type", "NETWORK_DEVICE") mandatory_params_absent = [] if device_type == "NETWORK_DEVICE": params_list = ["enablePassword", "ipAddress", "password", "snmpUserName", "snmpAuthPassphrase", "snmpPrivPassphrase", "userName"] - for param in params_list: - if param not in config: - mandatory_params_absent.append(param) - - if mandatory_params_absent: - self.msg = "Mandatory paramters {0} not present".format(mandatory_params_absent) - 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." - self.status = "success" + elif device_type == "COMPUTE_DEVICE": + params_list = ["ipAddress", "httpUserName", "httpPassword", "httpPort", "snmpUserName", "snmpAuthPassphrase", "snmpPrivPassphrase"] + + elif device_type == "MERAKI_DASHBOARD": + params_list = ["httpPassword"] + + elif device_type == "FIREPOWER_MANAGEMENT_SYSTEM": + params_list = ["ipAddress", "httpUserName", "httpPassword"] + + elif device_type == "THIRD_PARTY_DEVICE": + params_list = ["ipAddress", "snmpUserName", "snmpAuthPassphrase", "snmpPrivPassphrase"] + + for param in params_list: + if param not in config: + mandatory_params_absent.append(param) + + if mandatory_params_absent: + self.msg = "Mandatory paramters {0} not present".format(mandatory_params_absent) + 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." + self.status = "success" return self @@ -469,6 +529,7 @@ def get_device_params(self, params): """ device_param = { + "cli_transport": params.get("cliTransport"), "enable_password": params.get("enablePassword"), "password": params.get("password"), "ipaddress": params.get("ipAddress"), @@ -542,6 +603,7 @@ def get_diff_merged(self, config): # device_resynced = False devices_to_add = self.have["device_not_in_dnac"] + device_type = self.config[0].get('type') self.result['log'] = [] if not devices_to_add: @@ -557,7 +619,9 @@ def get_diff_merged(self, config): # If we want to add device in inventory self.mandatory_parameter(config).check_return_status() config['ipAddress'] = devices_to_add - del config['resync'] + config['type'] = device_type + if device_type == "FIREPOWER_MANAGEMENT_SYSTEM": + config['httpPort'] = self.config[0].get("httpPort", "443") try: response = self.dnac._exec( From 9c1b25ab358328ca013e54d6c83cb965899e429a Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 26 Oct 2023 16:09:14 +0530 Subject: [PATCH 07/32] fix ansible sanity failed testcases --- plugins/modules/inventory_intent.py | 139 ++++++++++++++++++---------- 1 file changed, 89 insertions(+), 50 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index f599449222..22a45e6bb6 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -47,17 +47,16 @@ description: Network Device's extendedDiscoveryInfo. type: str httpPassword: - description: Network Device's httpPassword.Required for Adding Compute, Meraki, - Firepower Management System Devices. + description: Network Device's httpPassword.Required for Adding Compute, Meraki, Firepower Management Devices. type: str httpPort: - description: Network Device's httpPort.Required for Adding Compute, Firepower Management System Devices. + description: Network Device's httpPort.Required for Adding Compute, Firepower Management Devices. type: str httpSecure: description: HttpSecure flag. type: bool httpUserName: - description: Network Device's httpUserName.Required for Adding Compute,Firepower Management System Devices. + description: Network Device's httpUserName.Required for Adding Compute,Firepower Management Devices. type: str id: description: Id path parameter. Device ID.Required for Deleting Device. @@ -212,51 +211,91 @@ userName: string - name: Add new Compute device in Inventory with full credentials.Inputs needed for Compute Device - 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 - resync: false - type: "COMPUTE_DEVICE" - -- name: Add new Meraki device in Inventory with full credentials.Inputs needed for Meraki Device - config: - - httpPassword: string - resync: false - type: "MERAKI_DASHBOARD" - -- name: Add new Firepower Management System device in Inventory with full credentials.Input needed for Firepower Management System Device - config: - - ipAddress: string - httpUserName: string - httpPassword: string - httpPort: string - resync: false - type: "FIREPOWER_MANAGEMENT_SYSTEM" - -- name: Add new Third Party device in Inventory with full credentials.Input needed for Third Party Device - config: - - ipAddress: string - snmpAuthPassphrase: string - snmpAuthProtocol: string - snmpMode: string - snmpPrivPassphrase: string - snmpPrivProtocol: string - snmpRetry: 3 - snmpTimeout: 5 - snmpUserName: string - resync: false - type: "THIRD_PARTY_DEVICE" + 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: + - 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 + resync: false + type: "COMPUTE_DEVICE" + +- name: Add new Meraki device in Inventory with full credentials.Inputs needed for Meraki Device. + 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: + - httpPassword: string + resync: false + type: "MERAKI_DASHBOARD" + +- name: Add new Firepower Management device in Inventory with full credentials.Input needed to add Device. + 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: + - ipAddress: string + httpUserName: string + httpPassword: string + httpPort: string + resync: false + type: "FIREPOWER_MANAGEMENT_SYSTEM" + +- name: Add new Third Party device in Inventory with full credentials.Input needed to add Device. + 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: + - ipAddress: string + snmpAuthPassphrase: string + snmpAuthProtocol: string + snmpMode: string + snmpPrivPassphrase: string + snmpPrivProtocol: string + snmpRetry: 3 + snmpTimeout: 5 + snmpUserName: string + resync: false + type: "THIRD_PARTY_DEVICE" - name: Delete Device by id cisco.dnac.inventory_intent: @@ -455,7 +494,7 @@ def mandatory_parameter(self, config): elif device_type == "COMPUTE_DEVICE": params_list = ["ipAddress", "httpUserName", "httpPassword", "httpPort", "snmpUserName", "snmpAuthPassphrase", "snmpPrivPassphrase"] - + elif device_type == "MERAKI_DASHBOARD": params_list = ["httpPassword"] From cfad03e2d41bc39494f090c9cbe6c2d64395cfda Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 27 Oct 2023 06:04:31 +0000 Subject: [PATCH 08/32] PnP with non mandatory image and template --- playbooks/PnP.yml | 53 ++++++++ plugins/modules/pnp_intent.py | 239 ++++++++++++++++++++++++++++------ 2 files changed, 253 insertions(+), 39 deletions(-) create mode 100644 playbooks/PnP.yml diff --git a/playbooks/PnP.yml b/playbooks/PnP.yml new file mode 100644 index 0000000000..954c2bbb12 --- /dev/null +++ b/playbooks/PnP.yml @@ -0,0 +1,53 @@ +--- +- name: Manage operations add device, claim device and unclaim device of Onboarding Configuration(PnP) + 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: Add a new device and claim the device + cisco.dnac.pnp_intent: + <<: *dnac_login + dnac_log: True + state: deleted + config: + - template_name: "Ansible_PNP_WLC" + image_name: C9800-40-universalk9_wlc.17.12.01.SPA.bin + golden_image: True + site_name: Global/USA/San Francisco/BGL_18 + pnp_type: CatalystWLC + deviceInfo: + serialNumber: FOX2639PAY7 + hostname: WLC + state: Unclaimed + pid: C9800-40-K9 + projectName: Onboarding Configuration + staticIP: 204.192.101.10 + subnetMask: 255.255.255.0 + gateway: 204.192.101.1 + vlanId: 1101 + ipInterfaceName: TenGigabitEthernet0/0/0 + + - template_name: "Ansible_PNP_Switch" + image_name: cat9k_iosxe_npe.17.03.07.SPA.bin + site_name: Global/USA/San Francisco/BGL_18 + deviceInfo: + serialNumber: FJC271924EQ + hostname: Switch + state: Unclaimed + pid: C9300-48UXM + projectName: Onboarding Configuration diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 6ce4a54256..5a289ad27c 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -3,11 +3,10 @@ # 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__ = ("Madhan Sankaranarayanan, Rishita Chowdhary") +__author__ = ("Madhan Sankaranarayanan, Rishita Chowdhary, Abinash Mishra") DOCUMENTATION = r""" --- @@ -22,6 +21,7 @@ - cisco.dnac.intent_params author: Madhan Sankaranarayanan (@madhansansel) Rishita Chowdhary (@rishitachowdhary) + Abinash Mishra (@abimishr) options: state: description: The state of DNAC after module completion. @@ -624,7 +624,23 @@ def __init__(self, module): def validate_input(self): """ - Validate the fields provided in the playbook + 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: @@ -633,14 +649,21 @@ def validate_input(self): return self pnp_spec = { - 'template_name': {'type': 'str', 'required': True}, + 'template_name': {'type': 'str', 'required': False}, 'project_name': {'type': 'str', 'required': False, 'default': 'Onboarding Configuration'}, - 'site_name': {'type': 'str', 'required': True}, - 'image_name': {'type': 'str', 'required': True}, + 'site_name': {'type': 'str', 'required': False}, + 'image_name': {'type': 'str', 'required': False}, 'golden_image': {'type': 'bool', 'required': False}, 'deviceInfo': {'type': 'dict', 'required': True}, 'pnp_type': {'type': 'str', 'required': False, 'default': 'Default'}, + "rfProfile": {'type': 'str', 'required': False}, + "staticIP": {'type': 'str', 'required': False}, + "subnetMask": {'type': 'str', 'required': False}, + "gateway": {'type': 'str', 'required': False}, + "vlanId": {'type': 'str', 'required': False}, + "ipInterfaceName": {'type': 'str', 'required': False}, + "sensorProfile": {'type': 'str', 'required': False} } # Validate pnp params @@ -656,14 +679,25 @@ def validate_input(self): self.validated_config = valid_pnp self.log(str(valid_pnp)) + self.msg = "Successfully validated input" self.status = "success" + return self def site_exists(self): """ Check whether the site exists or not + 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: + - site_exits: A boolean value indicating the existence of the site. + - site_id: The Id of the site i.e. required to claim a device to site. + Example: + Post creation of the validated input, we this method gets the site_id and checks + whether the site exists or not """ site_exists = False @@ -687,10 +721,53 @@ def site_exists(self): return (site_exists, site_id) + def get_site_type(self): + + """ + Fetches the type of site + 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: + - 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": self.want.get("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_pnp_params(self, params): """ Store pnp parameters from the playbook for pnp processing in DNAC + Args: + self: The instance of the class containing the 'config' attribute to be validated. + params: The validated params passed from the playbook + Returns: + The method returns an instance of the class with updated attributes: + - pnp_params: A dictionary containing all the values indicating + the type of the site (area/building/floor). + 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 """ pnp_params = { @@ -710,6 +787,17 @@ def get_image_params(self, params): """ Get image name and the confirmation whether it's tagged golden or not + Args: + self: The instance of the class containing the 'config' attribute to be validated. + params: The validated params passed from the playbook + Returns: + The method returns an instance of the class with updated attributes: + - image_params: A dictionary containing all the values indicating + name of the image and its golden image status. + 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 """ image_params = { @@ -721,7 +809,15 @@ def get_image_params(self, params): def get_claim_params(self): """ - Get the paramters needed for claiming + Get the paramters needed for claiming the device to site + 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: + - claim_params: A dictionary needed for calling the POST call for claim + a device to a site API + Example: + The stored dictionary can be used to call the API claim a device to a site via SDK """ imageinfo = { @@ -747,12 +843,32 @@ 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'] + 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'] + + if claim_params["type"] == "AccessPoint": + claim_params["rfProfile"] = self.validated_config[0]["rfProfile"] + return claim_params def get_have(self): """ Get the current image, template and site details from the DNAC + 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.image_response: A list of image passed by the user + - self.template_list: A list of template under project + - self.device_response: Gets the device_id and stores it + Example: + 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 = {} @@ -763,49 +879,47 @@ def get_have(self): function='get_software_image_details', params=self.want.get("image_params"), ) - - self.log(str(image_response)) - image_list = image_response.get("response") + self.log(str(image_response)) - if len(image_list) == 1: - have["image_id"] = image_list[0].get("imageUuid") - self.log("Image Id: " + str(have["image_id"])) - else: - self.module.fail_json(msg="Image not found", response=[]) - - # check if given template exists, if exists store template id + # 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)) - if template_list and isinstance(template_list, list): - # API execution error returns a dict - template_details = get_dict_result(template_list, - 'name', self.want.get("template_name")) - if template_details: - have["template_id"] = template_details.get("templateId") - - self.log("Template Id: " + str(have["template_id"])) - else: - self.module.fail_json(msg="Template not found", response=[]) - else: - self.module.fail_json(msg="Project Not Found", response=[]) - # check if given site exits, if exists store current site info - site_name = self.want.get("site_name") - site_exists = False - (site_exists, site_id) = self.site_exists() + if isinstance(self.want.get("site_name"), str): + site_name = self.want.get("site_name") + (site_exists, site_id) = self.site_exists() 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.module.fail_json(msg="Type of the site must \ + be a floor for claiming an AP", response=[]) + if len(image_list) == 1: + have["image_id"] = image_list[0].get("imageUuid") + self.log("Image Id: " + str(have["image_id"])) + if template_list and isinstance(template_list, list): + # API execution error returns a dict + if self.want.get("template_name"): + template_details = get_dict_result(template_list, 'name', self.want.get("template_name")) + + if template_details: + have["template_id"] = template_details.get("templateId") + else: + self.module.fail_json(msg="Template Not Found", response=[]) + else: + self.module.fail_json(msg="Project Not Found or Project is Empty", response=[]) + else: + self.module.fail_json(msg="Site not found", response=[]) # check if given device exists in pnp inventory, store device Id device_response = self.dnac_apply['exec']( @@ -833,8 +947,20 @@ def get_have(self): def get_want(self, config): """ - Get all the image, site and pnp related + Get all the image, template and site and pnp related information from playbook that is needed to be created in DNAC + 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.msgt: 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 = { @@ -848,6 +974,17 @@ def get_want(self, config): 'template_name': config.get('template_name') } + if self.want["pnp_type"] == "CatalystWLC": + self.want["staticIP"] = config.get('staticIP') + self.want["subnetMask"] = config.get('subnetMask') + self.want["gateway"] = config.get('gateway') + self.want["vlanId"] = config.get('vlanId') + self.want["ipInterfaceName"] = config.get('ipInterfaceName') + + if self.want["pnp_type"] == "AccessPoint": + if self.get_site_type == "floor": + self.want["rfProfile"] = config.get("rfProfile") + self.msg = "Successfully collected all parameters from playbook " + \ "for comparison" self.status = "success" @@ -859,6 +996,16 @@ def get_diff_merged(self): """ If given device doesnot exist then add it to pnp database and get the device id + 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 not self.have.get("device_found"): @@ -869,8 +1016,8 @@ def get_diff_merged(self): params=self.want.get("pnp_params"), op_modifies=True, ) - self.have["device_id"] = response.get("id") + self.have["device_id"] = response.get("id") self.log(str(response)) self.log(self.have.get("device_id")) @@ -888,15 +1035,29 @@ def get_diff_merged(self): self.result['changed'] = True self.result['msg'] = "Device Claimed Successfully" self.result['response'] = claim_response - self.result['diff'] = self.validated + self.result['diff'] = self.validated_config else: self.module.fail_json(msg="Device Claim Failed", response=claim_response) return self def get_diff_deleted(self): - if self.have.get("device_found"): + """ + If the given device is added to pnp database + 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 + 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. + """ + + if self.have.get("device_found"): try: response = self.dnac_apply['exec']( family="device_onboarding_pnp", @@ -910,7 +1071,7 @@ def get_diff_deleted(self): if response.get("deviceInfo").get("state") == "Deleted": self.result['changed'] = True self.result['response'] = response - self.result['diff'] = self.validated + self.result['diff'] = self.validated_config self.result['msg'] = "Device Deleted Successfully" else: self.result['response'] = response From a844d41c90f68e6a79e1ed40effe1a1fe0246a2b Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Fri, 27 Oct 2023 08:59:57 +0000 Subject: [PATCH 09/32] Inheriting the base class in each unit test module --- tests/unit/modules/dnac/dnac_module.py | 37 +++++++++++++++++++ tests/unit/modules/dnac/test_pnp_intent.py | 32 ++++------------ tests/unit/modules/dnac/test_site_intent.py | 33 ++++------------- tests/unit/modules/dnac/test_swim_intent.py | 31 ++++------------ .../unit/modules/dnac/test_template_intent.py | 31 ++++------------ 5 files changed, 65 insertions(+), 99 deletions(-) diff --git a/tests/unit/modules/dnac/dnac_module.py b/tests/unit/modules/dnac/dnac_module.py index c05b5a6eed..85deb42ceb 100644 --- a/tests/unit/modules/dnac/dnac_module.py +++ b/tests/unit/modules/dnac/dnac_module.py @@ -28,6 +28,7 @@ from ansible_collections.ansible.netcommon.tests.unit.modules.utils import ( set_module_args as _set_module_args, ) +from unittest.mock import patch def set_module_args(args): @@ -75,6 +76,42 @@ def load_fixture(module_name, name, device=""): class TestDnacModule(ModuleTestCase): + + def __init__(self, module): + self.module = module + self.test_data = self.loadPlaybookData(str(self.module.__name__)) + self.playbook_config = self.test_data.get("playbook_config") + self.playbook_config_missing_param = self.test_data.get("playbook_config_missing_param") + + def setUp(self): + self.mock_dnac_init = patch( + "ansible_collections.cisco.dnac.plugins.module_utils.dnac.DNACSDK.__init__") + self.run_dnac_init = self.mock_dnac_init.start() + self.run_dnac_init.side_effect = [None] + self.mock_dnac_exec = patch( + "ansible_collections.cisco.dnac.plugins.module_utils.dnac.DNACSDK.exec" + ) + self.run_dnac_exec = self.mock_dnac_exec.start() + + def tearDown(self): + self.mock_dnac_exec.stop() + self.mock_dnac_init.stop() + + def loadPlaybookData(self, module): + path = os.path.join(fixture_path, "{0}.json".format(module)) + print(path) + + with open(path) as f: + data = f.read() + + try: + j_data = json.loads(data) + except Exception as e: + print(e) + pass + + return j_data + def execute_module_devices( self, failed=False, changed=False, response=None, sort=True, defaults=False ): diff --git a/tests/unit/modules/dnac/test_pnp_intent.py b/tests/unit/modules/dnac/test_pnp_intent.py index b3c15a4ff4..af2ef09159 100644 --- a/tests/unit/modules/dnac/test_pnp_intent.py +++ b/tests/unit/modules/dnac/test_pnp_intent.py @@ -17,37 +17,19 @@ __metaclass__ = type -from unittest.mock import patch from ansible.errors import AnsibleActionFail from ansible_collections.cisco.dnac.plugins.modules import pnp_intent -from .dnac_module import TestDnacModule, set_module_args, loadPlaybookData +from .dnac_module import TestDnacModule, set_module_args class TestDnacPnPIntent(TestDnacModule): + def __init__(self): + """ + Inheriting from the base class of dnac_module + """ - module = pnp_intent - - test_data = loadPlaybookData("pnp_intent") - - playbook_config = test_data.get("playbook_config") - playbook_config_missing_param = test_data.get("playbook_config_missing_param") - - def setUp(self): - super(TestDnacPnPIntent, self).setUp() - - self.mock_dnac_init = patch( - "ansible_collections.cisco.dnac.plugins.module_utils.dnac.DNACSDK.__init__") - self.run_dnac_init = self.mock_dnac_init.start() - self.run_dnac_init.side_effect = [None] - self.mock_dnac_exec = patch( - "ansible_collections.cisco.dnac.plugins.module_utils.dnac.DNACSDK.exec" - ) - self.run_dnac_exec = self.mock_dnac_exec.start() - - def tearDown(self): - super(TestDnacPnPIntent, self).tearDown() - self.mock_dnac_exec.stop() - self.mock_dnac_init.stop() + module = pnp_intent + super().__init__(module) def load_fixtures(self, response=None, device=""): if "site_not_found" in self._testMethodName: diff --git a/tests/unit/modules/dnac/test_site_intent.py b/tests/unit/modules/dnac/test_site_intent.py index a12f1d1c44..3de7357054 100644 --- a/tests/unit/modules/dnac/test_site_intent.py +++ b/tests/unit/modules/dnac/test_site_intent.py @@ -17,37 +17,18 @@ __metaclass__ = type -from unittest.mock import patch - from ansible_collections.cisco.dnac.plugins.modules import site_intent -from .dnac_module import TestDnacModule, set_module_args, loadPlaybookData +from .dnac_module import TestDnacModule, set_module_args class TestDnacSiteIntent(TestDnacModule): + def __init__(self): + """ + Inheriting from the base class of dnac_module + """ - module = site_intent - - test_data = loadPlaybookData("site_intent") - - playbook_config = test_data.get("playbook_config") - playbook_config_missing_param = test_data.get("playbook_config_missing_param") - - def setUp(self): - super(TestDnacSiteIntent, self).setUp() - - self.mock_dnac_init = patch( - "ansible_collections.cisco.dnac.plugins.module_utils.dnac.DNACSDK.__init__") - self.run_dnac_init = self.mock_dnac_init.start() - self.run_dnac_init.side_effect = [None] - self.mock_dnac_exec = patch( - "ansible_collections.cisco.dnac.plugins.module_utils.dnac.DNACSDK.exec" - ) - self.run_dnac_exec = self.mock_dnac_exec.start() - - def tearDown(self): - super(TestDnacSiteIntent, self).tearDown() - self.mock_dnac_exec.stop() - self.mock_dnac_init.stop() + module = site_intent + super().__init__(module) def load_fixtures(self, response=None, device=""): if "create_site" in self._testMethodName: diff --git a/tests/unit/modules/dnac/test_swim_intent.py b/tests/unit/modules/dnac/test_swim_intent.py index 18acf4762f..a9ef4fa33b 100644 --- a/tests/unit/modules/dnac/test_swim_intent.py +++ b/tests/unit/modules/dnac/test_swim_intent.py @@ -17,35 +17,18 @@ __metaclass__ = type -from unittest.mock import patch - from ansible_collections.cisco.dnac.plugins.modules import swim_intent -from .dnac_module import TestDnacModule, set_module_args, loadPlaybookData +from .dnac_module import TestDnacModule, set_module_args class TestDnacSwimIntent(TestDnacModule): + def __init__(self): + """ + Inheriting from the base class of dnac_module + """ - module = swim_intent - test_data = loadPlaybookData("swim_intent") - playbook_config = test_data.get("playbook_config") - playbook_config_untag_image = test_data.get("playbook_config_untag_golden_image") - - def setUp(self): - super(TestDnacSwimIntent, self).setUp() - - self.mock_dnac_init = patch( - "ansible_collections.cisco.dnac.plugins.module_utils.dnac.DNACSDK.__init__") - self.run_dnac_init = self.mock_dnac_init.start() - self.run_dnac_init.side_effect = [None] - self.mock_dnac_exec = patch( - "ansible_collections.cisco.dnac.plugins.module_utils.dnac.DNACSDK.exec" - ) - self.run_dnac_exec = self.mock_dnac_exec.start() - - def tearDown(self): - super(TestDnacSwimIntent, self).tearDown() - self.mock_dnac_exec.stop() - self.mock_dnac_init.stop() + module = swim_intent + super().__init__(module) def load_fixtures(self, response=None, device=""): if "full_flow" in self._testMethodName: diff --git a/tests/unit/modules/dnac/test_template_intent.py b/tests/unit/modules/dnac/test_template_intent.py index c0f866d460..b625118859 100644 --- a/tests/unit/modules/dnac/test_template_intent.py +++ b/tests/unit/modules/dnac/test_template_intent.py @@ -17,35 +17,18 @@ __metaclass__ = type -from unittest.mock import patch from ansible_collections.cisco.dnac.plugins.modules import template_intent -from .dnac_module import TestDnacModule, set_module_args, loadPlaybookData +from .dnac_module import TestDnacModule, set_module_args class TestDnacTemplateIntent(TestDnacModule): + def __init__(self): + """ + Inheriting from the base class of dnac_module + """ - module = template_intent - - test_data = loadPlaybookData("template_intent") - - playbook_config = test_data.get("playbook_config") - playbook_config_missing_param = test_data.get("playbook_config_missing_param") - - def setUp(self): - super(TestDnacTemplateIntent, self).setUp() - self.mock_dnac_init = patch( - "ansible_collections.cisco.dnac.plugins.module_utils.dnac.DNACSDK.__init__") - self.run_dnac_init = self.mock_dnac_init.start() - self.run_dnac_init.side_effect = [None] - self.mock_dnac_exec = patch( - "ansible_collections.cisco.dnac.plugins.module_utils.dnac.DNACSDK.exec" - ) - self.run_dnac_exec = self.mock_dnac_exec.start() - - def tearDown(self): - super(TestDnacTemplateIntent, self).tearDown() - self.mock_dnac_exec.stop() - self.mock_dnac_init.stop() + module = template_intent + super().__init__(module) def load_fixtures(self, response=None, device=""): if "create_template" in self._testMethodName: From 8d23d6d59e76b50155580a15f7ead6135b2c3ad2 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Wed, 1 Nov 2023 23:57:48 +0530 Subject: [PATCH 10/32] Added device credential intent module and playbook --- playbooks/device_credential_intent.yml | 85 + plugins/modules/device_credential_intent.py | 2323 +++++++++++++++++++ plugins/modules/network_settings_intent.py | 93 +- 3 files changed, 2458 insertions(+), 43 deletions(-) create mode 100644 playbooks/device_credential_intent.yml create mode 100644 plugins/modules/device_credential_intent.py diff --git a/playbooks/device_credential_intent.yml b/playbooks/device_credential_intent.yml new file mode 100644 index 0000000000..13dbbb3425 --- /dev/null +++ b/playbooks/device_credential_intent.yml @@ -0,0 +1,85 @@ +- hosts: dnac_servers + vars_files: + - credentials_245.yml + gather_facts: no + connection: local + tasks: +# +# Project Info Section +# + + - name: Create Credentials and assign it to a site. + 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: merged + config: + - GlobalCredentialDetails: + cliCredential: + - description: CLI + username: cli + password: "12345" + enablePassword: "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" + # 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" + # 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 + # old_description: + # id: d8974823-250a-41b0-8c9b-b27b2ae01472 # Use this for updation or deletion + httpsRead: + - description: HTTP Read + username: HTTP_Read + 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 + 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: + - Global/Chennai/Trill + - Global/Chennai/Tidel diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py new file mode 100644 index 0000000000..e75ea79ccb --- /dev/null +++ b/plugins/modules/device_credential_intent.py @@ -0,0 +1,2323 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2023, 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 device credentials in Cisco DNA Center.""" +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = ['Muthu Rakesh, Madhan Sankaranarayanan'] + +DOCUMENTATION = r""" +--- +module: device_credential_intent +short_description: Resource module for Global Device Credentials and Assigning Credentials to sites. +description: +- Manage operations on Global Device Credentials and Assigning Credentials to sites. +- API to create global device credentials. +- API to update global device credentials. +- API to delete global device credentials. +- API to assign the device credential to the site. +version_added: '6.7.0' +extends_documentation_fragment: + - cisco.dnac.intent_params +author: Muthu Rakesh (@MUTHU-RAKESH-27) + Madhan Sankaranarayanan (@madhansansel) +options: + state: + description: The state of Cisco DNA Center after module completion. + type: str + choices: [ merged, deleted ] + default: merged + config: + description: + - List of details of global device credentials and site names. + type: list + elements: dict + required: true + suboptions: + GlobalCredentialDetails: + description: Manages global device credentials + type: dict + suboptions: + cliCredential: + description: Global Credential V2's cliCredential. + type: list + elements: dict + suboptions: + description: + description: Description. + type: str + enablePassword: + description: Enable Password. + type: str + id: + description: Id. + type: str + password: + description: Password. + type: str + username: + description: Username. + type: str + old_description: + description: Old Description + type: str + old_username: + description: Old Username + type: str + httpsRead: + description: Global Credential V2's httpsRead. + type: list + elements: dict + suboptions: + id: + description: Id. + type: str + name: + description: Name. + type: str + password: + description: Password. + type: str + port: + description: Port. + type: int + username: + description: Username. + type: str + old_description: + description: Old Description + type: str + old_username: + description: Old Username + type: str + httpsWrite: + description: Global Credential V2's httpsWrite. + type: list + elements: dict + suboptions: + id: + description: Id. + type: str + name: + description: Name. + type: str + password: + description: Password. + type: str + port: + description: Port. + type: int + username: + description: Username. + type: str + old_description: + description: Old Description + type: str + old_username: + description: Old Username + type: str + snmpV2cRead: + description: Global Credential V2's snmpV2cRead. + type: list + elements: dict + suboptions: + description: + description: Description. + type: str + id: + description: Id. + type: str + readCommunity: + description: Read Community. + type: str + old_description: + description: Old Description. + type: str + snmpV2cWrite: + description: Global Credential V2's snmpV2cWrite. + type: list + elements: dict + suboptions: + description: + description: Description. + type: str + id: + description: Id. + type: str + writeCommunity: + description: Write Community. + type: str + old_description: + description: Old Description. + type: str + snmpV3: + description: Global Credential V2's snmpV3. + type: list + elements: dict + suboptions: + authPassword: + description: Auth Password. + type: str + authType: + description: Auth Type. + type: str + description: + description: Description. + type: str + id: + description: Id. + type: str + privacyPassword: + description: Privacy Password. + type: str + privacyType: + description: Privacy Type. + type: str + snmpMode: + description: Snmp Mode. + type: str + username: + description: Username. + type: str + AssignCredentialsToSite: + description: Assign Device Credentials to Site. + type: dict + suboptions: + cliDescription: + description: CLI Credential Description. + type: str + cliUsername: + description: CLI Credential Username. + type: str + cliId: + description: CLI Credential Id. + type: str + httpReadDescription: + description: HTTP(S) Read Credential Description. + type: str + httpReadUsername: + description: HTTP(S) Read Credential Username. + type: str + httpRead: + description: HTTP(S) Read Credential Id. + type: str + httpWriteDescription: + description: HTTP(S) Write Credential Description. + type: str + httpWriteUsername: + description: HTTP(S) Write Credential Username. + type: str + httpWrite: + description: HTTP(S) Write Credential Id. + type: str + siteName: + description: Site Name to assign credential. + type: list + elements: str + snmpV2ReadDescription: + description: SNMPv2c Read Credential Description. + type: str + snmpV2ReadId: + description: SNMPv2c Read Credential Id. + type: str + snmpV2WriteDescription: + description: SNMPv2c Write Credential Description. + type: str + snmpV2WriteId: + description: SNMPv2c Write Credential Id. + type: str + snmpV3Description: + description: SNMPv3 Credential Description. + type: str + snmpV3Id: + description: SNMPv3 Credential Id. + type: str +requirements: +- dnacentersdk >= 2.5.5 +- python >= 3.5 +seealso: +- name: Cisco DNA Center documentation for Discovery CreateGlobalCredentialsV2 + description: Complete reference of the CreateGlobalCredentialsV2 API. + link: https://developer.cisco.com/docs/dna-center/#!create-global-credentials-v-2 +- name: Cisco DNA Center documentation for Discovery DeleteGlobalCredentialV2 + description: Complete reference of the DeleteGlobalCredentialV2 API. + link: https://developer.cisco.com/docs/dna-center/#!delete-global-credential-v-2 +- name: Cisco DNA Center documentation for Discovery UpdateGlobalCredentialsV2 + description: Complete reference of the UpdateGlobalCredentialsV2 API. + link: https://developer.cisco.com/docs/dna-center/#!update-global-credentials-v-2 +- name: Cisco DNA Center documentation for Network Settings AssignDeviceCredentialToSiteV2 + description: Complete reference of the AssignDeviceCredentialToSiteV2 API. + link: https://developer.cisco.com/docs/dna-center/#!assign-device-credential-to-site-v-2 +notes: + - SDK Method used are + discovery.Discovery.create_global_credentials_v2, + discovery.Discovery.delete_global_credential_v2, + discovery.Discovery.update_global_credentials_v2, + network_settings.NetworkSettings.assign_device_credential_to_site_v2, + + - Paths used are + post /dna/intent/api/v2/global-credential, + delete /dna/intent/api/v2/global-credential/{id}, + put /dna/intent/api/v2/global-credential, + post /dna/intent/api/v2/credential-to-site/{siteId}, +""" + +EXAMPLES = r""" +- name: Create Credentials and assign it to a site. + 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: merged + config: + - GlobalCredentialDetails: + cliCredential: + - description: string + username: string + password: string + enablePassword: string + snmpV2cRead: + - description: string + readCommunity: string + snmpV2cWrite: + - description: string + writeCommunity: string + snmpV3: + - authPassword: string + authType: SHA + snmpMode: AUTHPRIV + privacyPassword: string + privacyType: AES128 + username: string + description: string + httpsRead: + - description: string + username: string + password: string + port: 443 + httpsWrite: + - description: string + username: string + password: string + port: 443 + AssignCredentialsToSite: + cliId: string + snmpV2ReadId: string + snmpV2WriteId: string + snmpV3Id: string + httpRead: string + httpWrite: string + siteName: + - string + +- name: Create Multiple 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: merged + config: + - GlobalCredentialDetails: + cliCredential: + - description: string + username: string + password: string + enablePassword: string + - description: string + username: string + password: string + enablePassword: string + snmpV2cRead: + - description: string + readCommunity: string + - description: string + readCommunity: string + snmpV2cWrite: + - description: string + writeCommunity: string + - description: string + writeCommunity: string + snmpV3: + - authPassword: string + authType: SHA + snmpMode: AUTHPRIV + privacyPassword: string + privacyType: AES128 + username: string + description: string + - authPassword: string + authType: SHA + snmpMode: AUTHPRIV + privacyPassword: string + privacyType: AES128 + username: string + description: string + httpsRead: + - description: string + username: string + password: string + port: 443 + - description: string + username: string + password: string + port: 443 + httpsWrite: + - description: string + username: string + password: string + port: 443 + - description: string + username: string + password: string + port: 443 + +- name: Update global device credentials using id + 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: merged + config: + - GlobalCredentialDetails: + cliCredential: + - description: string + username: string + password: string + enablePassword: string + id: string + snmpV2cRead: + - description: string + readCommunity: string + id: string + snmpV2cWrite: + - description: string + writeCommunity: string + id: string + snmpV3: + - authPassword: string + authType: SHA + snmpMode: AUTHPRIV + privacyPassword: string + privacyType: AES128 + username: string + description: string + id: string + httpsRead: + - description: string + username: string + password: string + port: 443 + id: string + httpsWrite: + - description: string + username: string + password: string + port: 443 + id: string + +- name: Update multiple global device credentials using id + 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: merged + config: + - GlobalCredentialDetails: + cliCredential: + - description: string + username: string + password: string + enablePassword: string + id: string + - description: string + username: string + password: string + enablePassword: string + id: string + snmpV2cRead: + - description: string + readCommunity: string + id: string + - description: string + readCommunity: string + id: string + snmpV2cWrite: + - description: string + writeCommunity: string + id: string + - description: string + writeCommunity: string + id: string + snmpV3: + - authPassword: string + authType: SHA + snmpMode: AUTHPRIV + privacyPassword: string + privacyType: AES128 + username: string + description: string + id: string + - authPassword: string + authType: SHA + snmpMode: AUTHPRIV + privacyPassword: string + privacyType: AES128 + username: string + description: string + id: string + httpsRead: + - description: string + username: string + password: string + port: 443 + id: string + - description: string + username: string + password: string + port: 443 + id: string + httpsWrite: + - description: string + username: string + password: string + port: 443 + id: string + - description: string + username: string + password: string + port: 443 + id: string + +- name: Update global device credential name/description using old name and description. + 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: merged + config: + - GlobalCredentialDetails: + cliCredential: + - description: string + username: string + password: string + enablePassword: string + old_description: string + old_username: string + snmpV2cRead: + - description: string + readCommunity: string + old_description: string + snmpV2cWrite: + - description: string + writeCommunity: string + old_description: string + snmpV3: + - authPassword: string + authType: string + snmpMode: string + privacyPassword: string + privacyType: string + username: string + description: string + httpsRead: + - description: string + username: string + password: string + port: string + old_description: string + old_username: string + httpsWrite: + - description: string + username: string + password: string + port: string + old_description: string + old_username: string + +- name: Assign Credentials to sites using old description and username. + 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: merged + config: + - AssignCredentialsToSite: + cliDescription: string + cliUsername: string + snmpV2ReadDescription: string + snmpV2WriteDescription: string + snmpV3Description: string + httpReadDescription: string + httpReadUsername: string + httpWriteUsername: string + httpWriteDescription: string + siteName: + - string + - string + +""" + +RETURN = r""" +# Case_1: Successful creation/updation/deletion of global device credentials +dnac_response1: + description: A dictionary or list with the response returned by the Cisco DNAC Python SDK + returned: always + type: dict + sample: > + { + "response": { + "taskId": "string", + "url": "string" + }, + "version": "string" + } + +# Case_2: Successful assignment of global device credentials to a site. +dnac_response2: + description: A dictionary or list with the response returned by the Cisco DNAC Python SDK + returned: always + type: dict + sample: > + { + "response": { + "taskId": "string", + "url": "string" + }, + "version": "string" + } +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( + DnacBase, + validate_list_of_dicts, + get_dict_result, +) + + +class DnacCredential(DnacBase): + """Class containing member attributes for device credential intent module""" + + def __init__(self, module): + super().__init__(module) + self.result["response"] = [ + { + "globalCredential": {}, + "assignCredential": {} + } + ] + + 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 '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 not self.config: + self.msg = "config not available in playbook for validation" + self.status = "success" + return self + + # temp_spec is the specification for the expected structure of configuration parameters + temp_spec = { + "GlobalCredentialDetails": { + "type": 'dict', + "cliCredential": { + "type": 'list', + "description": {"type": 'string'}, + "username": {"type": 'string'}, + "password": {"type": 'string'}, + "enablePassword": {"type": 'string'}, + "old_description": {"type": 'string'}, + "old_username": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "snmpV2cRead": { + "type": 'list', + "description": {"type": 'string'}, + "readCommunity": {"type": 'string'}, + "old_description": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "snmpV2cWrite": { + "type": 'list', + "description": {"type": 'string'}, + "writeCommunity": {"type": 'string'}, + "old_description": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "snmpV3": { + "type": 'list', + "description": {"type": 'string'}, + "username": {"type": 'string'}, + "snmpMode": {"type": 'string'}, + "authType": {"type": 'string'}, + "authPassword": {"type": 'string'}, + "privacyType": {"type": 'string'}, + "privacyPassword": {"type": 'string'}, + "old_description": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "httpsRead": { + "type": 'list', + "description": {"type": 'string'}, + "username": {"type": 'string'}, + "password": {"type": 'string'}, + "port": {"type": 'integer'}, + "old_description": {"type": 'string'}, + "old_username": {"type": 'string'}, + "id": {"type": 'string'}, + }, + "httpsWrite": { + "type": 'list', + "description": {"type": 'string'}, + "username": {"type": 'string'}, + "password": {"type": 'string'}, + "port": {"type": 'integer'}, + "old_description": {"type": 'string'}, + "old_username": {"type": 'string'}, + "id": {"type": 'string'}, + } + }, + "AssignCredentialsToSite": { + "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'} + } + } + + # Validate playbook params against the specification (temp_spec) + valid_temp, invalid_params = validate_list_of_dicts(self.config, temp_spec) + if invalid_params: + self.msg = "Invalid parameters in playbook: {0}".format("\n".join(invalid_params)) + self.status = "failed" + return self + + self.validated_config = valid_temp + self.log(str(valid_temp)) + self.msg = "Successfully validated input from the playbook" + self.status = "success" + return self + + def get_site_id(self, site_name): + """ + Get the site id from the site name. + Use check_return_status() to check for failure + + Parameters: + site_name (str) - Site name + + Returns: + str or None - The Site Id if found, or None if not found or error + """ + + try: + response = self.dnac._exec( + family="sites", + function='get_site', + params={"name": site_name}, + ) + self.log(str(response)) + if not response: + self.log("Failed to get the site id from site name {0}".format(site_name)) + return None + + _id = response.get("response")[0].get("id") + self.log("Site ID for the site name {0}".format(site_name) + str(_id)) + except Exception as exec: + self.log("Error while getting site_id from the site_name" + str(exec)) + return None + + return _id + + def get_global_credentials_params(self): + """ + Get the current Global Device Credentials from Cisco DNA Center. + + Parameters: + self - The current object details. + + Returns: + global_credentials (dict) - All global device credentials details + """ + + global_credentials = self.dnac._exec( + family="discovery", + function='get_all_global_credentials_v2', + ) + global_credentials = global_credentials.get("response") + self.log("All Global Device Credentials Details " + str(global_credentials)) + return global_credentials + + def get_cli_params(self, cliDetails): + """ + Format the CLI parameters for the CLI credential configuration in Cisco DNA Center. + + Parameters: + cliDetails (list of dict) - Cisco DNA Center details containing CLI Credentials. + + Returns: + cliCredential (list of dict) - Processed CLI credential data + in the format suitable for the Cisco DNA Center config. + """ + + cliCredential = [] + for item in cliDetails: + if item is None: + cliCredential.append(None) + else: + value = {} + value.update({ + "username": item.get("username"), + "description": item.get("description"), + "id": item.get("id") + }) + cliCredential.append(value) + return cliCredential + + def get_snmpV2cRead_params(self, snmpV2cReadDetails): + """ + Format the snmpV2cRead parameters for the snmpV2cRead + credential configuration in Cisco DNA Center. + + Parameters: + snmpV2cReadDetails (list of dict) - Cisco DNA Center + Details containing snmpV2cRead Credentials. + + Returns: + snmpV2cRead (list of dict) - Processed snmpV2cRead credential + data in the format suitable for the Cisco DNA Center config. + """ + + snmpV2cRead = [] + for item in snmpV2cReadDetails: + if item is None: + snmpV2cRead.append(None) + else: + value = {} + value.update({ + "description": item.get("description"), + "id": item.get("id") + }) + snmpV2cRead.append(value) + return snmpV2cRead + + def get_snmpV2cWrite_params(self, snmpV2cWriteDetails): + """ + Format the snmpV2cWrite parameters for the snmpV2cWrite + credential configuration in Cisco DNA Center. + + Parameters: + snmpV2cWriteDetails (list of dict) - Cisco DNA Center + Details containing snmpV2cWrite Credentials. + + Returns: + snmpV2cWrite (list of dict) - Processed snmpV2cWrite credential + data in the format suitable for the Cisco DNA Center config. + """ + + snmpV2cWrite = [] + for item in snmpV2cWriteDetails: + if item is None: + snmpV2cWrite.append(None) + else: + value = {} + value.update({ + "description": item.get("description"), + "id": item.get("id") + }) + snmpV2cWrite.append(value) + return snmpV2cWrite + + def get_httpsRead_params(self, httpsReadDetails): + """ + Format the httpsRead parameters for the httpsRead + credential configuration in Cisco DNA Center. + + Parameters: + httpsReadDetails (list of dict) - Cisco DNA Center + Details containing httpsRead Credentials. + + Returns: + httpsRead (list of dict) - Processed httpsRead credential + data in the format suitable for the Cisco DNA Center config. + """ + + httpsRead = [] + for item in httpsReadDetails: + if item is None: + httpsRead.append(None) + else: + value = {} + value.update({ + "description": item.get("description"), + "username": item.get("username"), + "port": item.get("port"), + "id": item.get("id") + }) + httpsRead.append(value) + return httpsRead + + def get_httpsWrite_params(self, httpsWriteDetails): + """ + Format the httpsWrite parameters for the httpsWrite + credential configuration in Cisco DNA Center. + + Parameters: + httpsWriteDetails (list of dict) - Cisco DNA Center + Details containing httpsWrite Credentials. + + Returns: + httpsWrite (list of dict) - Processed httpsWrite credential + data in the format suitable for the Cisco DNA Center config. + """ + + httpsWrite = [] + for item in httpsWriteDetails: + if item is None: + httpsWrite.append(None) + else: + value = {} + value.update({ + "description": item.get("description"), + "username": item.get("username"), + "port": item.get("port"), + "id": item.get("id") + }) + httpsWrite.append(value) + return httpsWrite + + def get_snmpV3_params(self, snmpV3Details): + """ + Format the snmpV3 parameters for the snmpV3 credential configuration in Cisco DNA Center. + + Parameters: + snmpV3Details (list of dict) - Cisco DNA Center details containing snmpV3 Credentials. + + Returns: + snmpV3 (list of dict) - Processed snmpV3 credential + data in the format suitable for the Cisco DNA Center config. + """ + + snmpV3 = [] + for item in snmpV3Details: + if item is None: + snmpV3.append(None) + else: + value = {} + value.update({ + "username": item.get("username"), + "description": item.get("description"), + "snmpMode": item.get("snmpMode"), + "id": item.get("id"), + }) + if value.get("snmpMode") == "AUTHNOPRIV": + value.update({ + "authType": item.get("authType") + }) + elif value.get("snmpMode") == "AUTHPRIV": + value.update({ + "authType": item.get("authType"), + "privacyType": item.get("privacyType") + }) + snmpV3.append(value) + return snmpV3 + + def get_have_device_credentials(self, CredentialDetails): + """ + Get the current Global Device Credentials from + Cisco DNA Center based on the provided playbook details. + Check this API using the check_return_status. + + Parameters: + CredentialDetails (dict) - Playbook details containing Global Device Credentials. + + Returns: + self - The current object with updated information. + """ + + global_credentials = self.get_global_credentials_params() + # playbook CLI Credential details + all_CLI = CredentialDetails.get("cliCredential") + # 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 + cliDetails = [] + if all_CLI and cli_details: + for cliCredential in all_CLI: + cliDetail = None + cliId = cliCredential.get("id") + if cliId: + cliDetail = get_dict_result(cli_details, "id", cliId) + if not cliDetail: + self.msg = "CLI credential ID is invalid" + self.status = "failed" + return self + + cliOldDescription = cliCredential.get("old_description") + cliOldUsername = cliCredential.get("old_username") + if cliOldDescription and cliOldUsername and (not cliDetail): + for item in cli_details: + if item.get("description") == cliOldDescription \ + and item.get("username") == cliOldUsername: + if cliDetail: + self.msg = "More than one CLI credential with same \ + old_description and old_username. Pass ID." + self.status = "failed" + return self + cliDetail = item + if not cliDetail: + self.msg = "CLI credential old_description or old_username is invalid" + self.status = "failed" + return self + + cliDescription = cliCredential.get("description") + cliUsername = cliCredential.get("username") + if cliDescription and cliUsername and (not cliDetail): + for item in cli_details: + if item.get("description") == cliDescription \ + and item.get("username") == cliUsername: + if cliDetail: + self.msg = "More than one CLI Credential with same \ + description and username. Pass ID." + self.status = "failed" + return self + cliDetail = item + cliDetails.append(cliDetail) + + # Playbook snmpV2cRead Credential details + all_snmpV2cRead = CredentialDetails.get("snmpV2cRead") + # 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 + snmpV2cReadDetails = [] + if all_snmpV2cRead and snmpV2cRead_details: + for snmpV2cReadCredential in all_snmpV2cRead: + snmpV2cReadDetail = None + snmpV2cReadId = snmpV2cReadCredential.get("id") + if snmpV2cReadId: + snmpV2cReadDetail = get_dict_result(snmpV2cRead_details, "id", snmpV2cReadId) + if not snmpV2cReadDetail: + self.msg = "snmpV2cRead credential ID is invalid" + self.status = "failed" + return self + + snmpV2cReadOldDescription = snmpV2cReadCredential.get("old_description") + if snmpV2cReadOldDescription and (not snmpV2cReadDetail): + snmpV2cReadDetail = get_dict_result( + snmpV2cRead_details, + "description", + snmpV2cReadOldDescription + ) + if not snmpV2cReadDetail: + self.msg = "snmpV2cRead credential old_description is invalid" + self.status = "failed" + return self + + snmpV2cReadDescription = snmpV2cReadCredential.get("description") + if snmpV2cReadDescription and (not snmpV2cReadDetail): + snmpV2cReadDetail = get_dict_result( + snmpV2cRead_details, + "description", + snmpV2cReadDescription + ) + snmpV2cReadDetails.append(snmpV2cReadDetail) + + # Playbook snmpV2cWrite Credential details + all_snmpV2cWrite = CredentialDetails.get("snmpV2cWrite") + # 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 + snmpV2cWriteDetails = [] + if all_snmpV2cWrite and snmpV2cWrite_details: + for snmpV2cWriteCredential in all_snmpV2cWrite: + snmpV2cWriteDetail = None + snmpV2cWriteId = snmpV2cWriteCredential.get("id") + if snmpV2cWriteId: + snmpV2cWriteDetail = get_dict_result(snmpV2cWrite_details, "id", snmpV2cWriteId) + if not snmpV2cWriteDetail: + self.msg = "snmpV2cWrite credential ID is invalid" + self.status = "failed" + return self + + snmpV2cWriteOldDescription = snmpV2cWriteCredential.get("old_description") + if snmpV2cWriteOldDescription and (not snmpV2cWriteDetail): + snmpV2cWriteDetail = get_dict_result( + snmpV2cWrite_details, + "description", + snmpV2cWriteOldDescription + ) + if not snmpV2cWriteDetail: + self.msg = "snmpV2cWrite credential old_description is invalid " + self.status = "failed" + return self + + snmpV2cWriteDescription = snmpV2cWriteCredential.get("description") + if snmpV2cWriteDescription and (not snmpV2cWriteDetail): + snmpV2cWriteDetail = get_dict_result( + snmpV2cWrite_details, + "description", + snmpV2cWriteDescription + ) + snmpV2cWriteDetails.append(snmpV2cWriteDetail) + + # Playbook httpsRead Credential details + all_httpsRead = CredentialDetails.get("httpsRead") + # 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 + httpsReadDetails = [] + if all_httpsRead and httpsRead_details: + for httpsReadCredential in all_httpsRead: + httpsReadDetail = None + httpsReadId = httpsReadCredential.get("id") + if httpsReadId: + httpsReadDetail = get_dict_result(httpsRead_details, "id", httpsReadId) + if not httpsReadDetail: + self.msg = "httpsRead credential Id is invalid" + self.status = "failed" + return self + + httpsReadOldDescription = httpsReadCredential.get("old_description") + httpsReadOldUsername = httpsReadCredential.get("old_username") + if httpsReadOldDescription and httpsReadOldUsername and (not httpsReadDetail): + for item in httpsRead_details: + if item.get("description") == httpsReadOldDescription \ + and item.get("username") == httpsReadOldUsername: + if httpsReadDetail: + self.msg = "More than one httpsRead credential with same \ + old_description and old_username. Pass ID." + self.status = "failed" + return self + httpsReadDetail = item + if not httpsReadDetail: + self.msg = "httpsRead credential old_description or old_username is invalid" + self.status = "failed" + return self + + httpsReadDescription = httpsReadCredential.get("description") + httpsReadUsername = httpsReadCredential.get("username") + if httpsReadDescription and httpsReadUsername and (not httpsReadDetail): + for item in httpsRead_details: + if item.get("description") == httpsReadDescription \ + and item.get("username") == httpsReadUsername: + if httpsReadDetail: + self.msg = "More than one httpsRead credential with same \ + description and username. Pass ID." + self.status = "failed" + return self + httpsReadDetail = item + httpsReadDetails.append(httpsReadDetail) + + # Playbook httpsWrite Credential details + all_httpsWrite = CredentialDetails.get("httpsWrite") + # 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 + httpsWriteDetails = [] + if all_httpsWrite and httpsWrite_details: + for httpsWriteCredential in all_httpsWrite: + httpsWriteDetail = None + httpsWriteId = httpsWriteCredential.get("id") + if httpsWriteId: + httpsWriteDetail = get_dict_result(httpsWrite_details, "id", httpsWriteId) + if not httpsWriteDetail: + self.msg = "httpsWrite credential Id is invalid" + self.status = "failed" + return self + + httpsWriteOldDescription = httpsWriteCredential.get("old_description") + httpsWriteOldUsername = httpsWriteCredential.get("old_username") + if httpsWriteOldDescription and httpsWriteOldUsername and (not httpsWriteDetail): + for item in httpsWrite_details: + if item.get("description") == httpsWriteOldDescription \ + and item.get("username") == httpsWriteOldUsername: + if httpsWriteDetail: + self.msg = "More than one httpsWrite credential with same \ + old_description and old_username. Pass ID" + self.status = "failed" + return self + httpsWriteDetail = item + if not httpsWriteDetail: + self.msg = "httpsWrite credential old_description or \ + old_username is invalid" + self.status = "failed" + return self + + httpsWriteDescription = httpsWriteCredential.get("description") + httpsWriteUsername = httpsWriteCredential.get("username") + if httpsWriteDescription and httpsWriteUsername and (not httpsWriteDetail): + for item in httpsWrite_details: + if item.get("description") == httpsWriteDescription \ + and item.get("username") == httpsWriteUsername: + httpsWriteDetail = item + httpsWriteDetails.append(httpsWriteDetail) + + # Playbook snmpV3 Credential details + all_snmpV3 = CredentialDetails.get("snmpV3") + # 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 + snmpV3Details = [] + if all_snmpV3 and snmpV3_details: + for snmpV3Credential in all_snmpV3: + snmpV3Detail = None + snmpV3Id = snmpV3Credential.get("id") + if snmpV3Id: + snmpV3Detail = get_dict_result(snmpV3_details, "id", snmpV3Id) + if not snmpV3Detail: + self.msg = "snmpV3 credential id is invalid" + self.status = "failed" + return self + + snmpV3OldDescription = snmpV3Credential.get("old_description") + if snmpV3OldDescription and (not snmpV3Detail): + snmpV3Detail = get_dict_result(snmpV3_details, + "description", snmpV3OldDescription) + if not snmpV3Detail: + self.msg = "snmpV3 credential old_description is invalid" + self.status = "failed" + return self + + snmpV3Description = snmpV3Credential.get("description") + if snmpV3Description and (not snmpV3Detail): + snmpV3Detail = get_dict_result(snmpV3_details, "description", snmpV3Description) + snmpV3Details.append(snmpV3Detail) + + self.have.update({"globalCredential": {}}) + if cliDetails: + cliCredential = self.get_cli_params(cliDetails) + self.have.get("globalCredential").update({"cliCredential": cliCredential}) + if snmpV2cReadDetails: + snmpV2cRead = self.get_snmpV2cRead_params(snmpV2cReadDetails) + self.have.get("globalCredential").update({"snmpV2cRead": snmpV2cRead}) + if snmpV2cWriteDetails: + snmpV2cWrite = self.get_snmpV2cWrite_params(snmpV2cWriteDetails) + self.have.get("globalCredential").update({"snmpV2cWrite": snmpV2cWrite}) + if httpsReadDetails: + httpsRead = self.get_httpsRead_params(httpsReadDetails) + self.have.get("globalCredential").update({"httpsRead": httpsRead}) + if httpsWriteDetails: + httpsWrite = self.get_httpsWrite_params(httpsWriteDetails) + self.have.get("globalCredential").update({"httpsWrite": httpsWrite}) + if snmpV3Details: + snmpV3 = self.get_snmpV3_params(snmpV3Details) + self.have.get("globalCredential").update({"snmpV3": snmpV3}) + + self.log("Global Device Credential Details " + str(self.have.get("globalCredential"))) + self.msg = "Collected the Global Device Credential Details from the Cisco DNA Center" + self.status = "success" + return self + + def get_have(self, config): + """ + Get the current Global Device Credentials and + Device Credentials assigned to a site in Cisco DNA Center. + + Parameters: + config (dict) - Playbook details containing Global Device + Credentials configurations and Device Credentials should + be assigned to a site. + + Returns: + self - The current object with updated information of Global + Device Credentials and Device Credentials assigned to a site. + """ + + if config.get("GlobalCredentialDetails") is not None: + CredentialDetails = config.get("GlobalCredentialDetails") + self.get_have_device_credentials(CredentialDetails).check_return_status() + + self.log("Credentials and Credentials Assigned to Site Details in Cisco DNA Center " + + str(self.have)) + self.msg = "Successfully retrieved the details from the Cisco DNA Center" + self.status = "success" + return self + + def get_want_device_credentials(self, CredentialDetails): + """ + Get the Global Device Credentials from the playbook. + Check this API using the check_return_status. + + Parameters: + CredentialDetails (dict) - Playbook details containing Global Device Credentials. + + Returns: + self - The current object with updated information of + Global Device Credentials from the playbook. + """ + + want = { + "want_create": {}, + "want_update": {} + } + if CredentialDetails.get("cliCredential"): + cli = CredentialDetails.get("cliCredential") + have_cli_ptr = 0 + create_cli_ptr = 0 + update_cli_ptr = 0 + values = ["password", "description", "username", "id"] + have_cliCredential = self.have.get("globalCredential").get("cliCredential") + for item in cli: + if not have_cliCredential or have_cliCredential[have_cli_ptr] is None: + if want.get("want_create").get("cliCredential") is None: + want.get("want_create").update({"cliCredential": []}) + create_credential = want.get("want_create").get("cliCredential") + create_credential.append({}) + for i in range(0, 3): + if item.get(values[i]): + create_credential[create_cli_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + self.msg = values[i] + " is mandatory for creating \ + cliCredential " + str(have_cli_ptr) + self.status = "failed" + return self + + if item.get("enablePassword"): + create_credential[create_cli_ptr] \ + .update({"enablePassword": item.get("enablePassword")}) + create_cli_ptr = create_cli_ptr + 1 + else: + if want.get("want_update").get("cliCredential") is None: + want.get("want_update").update({"cliCredential": []}) + update_credential = want.get("want_update").get("cliCredential") + update_credential.append({}) + if item.get("password"): + update_credential[update_cli_ptr] \ + .update({"password": item.get("password")}) + else: + self.msg = "password is mandatory for udpating \ + cliCredential " + str(have_cli_ptr) + self.status = "failed" + return self + + for i in range(1, 4): + if item.get(values[i]): + update_credential[update_cli_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + update_credential[update_cli_ptr].update({ + values[i]: self.have.get("globalCredential") + .get("cliCredential")[have_cli_ptr].get(values[i]) + }) + + if item.get("enablePassword"): + update_credential[update_cli_ptr].update({ + "enablePassword": item.get("enablePassword") + }) + update_cli_ptr = update_cli_ptr + 1 + have_cli_ptr = have_cli_ptr + 1 + + if CredentialDetails.get("snmpV2cRead"): + snmpV2cRead = CredentialDetails.get("snmpV2cRead") + have_snmpv2cread_ptr = 0 + create_snmpv2cread_ptr = 0 + update_snmpv2cread_ptr = 0 + values = ["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: + if want.get("want_create").get("snmpV2cRead") is None: + want.get("want_create").update({"snmpV2cRead": []}) + create_credential = want.get("want_create").get("snmpV2cRead") + create_credential.append({}) + for i in range(0, 2): + if item.get(values[i]): + create_credential[create_snmpv2cread_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + self.msg = values[i] + " is mandatory for creating \ + snmpV2cRead " + str(have_snmpv2cread_ptr) + self.status = "failed" + return self + create_snmpv2cread_ptr = create_snmpv2cread_ptr + 1 + else: + if want.get("want_update").get("snmpV2cRead") is None: + want.get("want_update").update({"snmpV2cRead": []}) + update_credential = want.get("want_update").get("snmpV2cRead") + update_credential.append({}) + if item.get("readCommunity"): + update_credential[update_snmpv2cread_ptr] \ + .update({"readCommunity": item.get("readCommunity")}) + else: + self.msg = "readCommunity is mandatory for updating \ + snmpV2cRead " + str(have_snmpv2cread_ptr) + self.status = "failed" + return self + for i in range(1, 3): + if item.get(values[i]): + update_credential[update_snmpv2cread_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + update_credential[update_snmpv2cread_ptr].update({ + values[i]: self.have.get("globalCredential") + .get("snmpV2cRead")[have_snmpv2cread_ptr].get(values[i]) + }) + update_snmpv2cread_ptr = update_snmpv2cread_ptr + 1 + have_snmpv2cread_ptr = have_snmpv2cread_ptr + 1 + + if CredentialDetails.get("snmpV2cWrite"): + snmpV2cWrite = CredentialDetails.get("snmpV2cWrite") + have_snmpv2cwrite_ptr = 0 + create_snmpv2cwrite_ptr = 0 + update_snmpv2cwrite_ptr = 0 + values = ["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: + if want.get("want_create").get("snmpV2cWrite") is None: + want.get("want_create").update({"snmpV2cWrite": []}) + create_credential = want.get("want_create").get("snmpV2cWrite") + create_credential.append({}) + for i in range(0, 2): + if item.get(values[i]): + create_credential[create_snmpv2cwrite_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + self.msg = values[i] + " is mandatory for creating \ + snmpV2cWrite " + str(have_snmpv2cwrite_ptr) + self.status = "failed" + return self + create_snmpv2cwrite_ptr = create_snmpv2cwrite_ptr + 1 + else: + if want.get("want_update").get("snmpV2cWrite") is None: + want.get("want_update").update({"snmpV2cWrite": []}) + update_credential = want.get("want_update").get("snmpV2cWrite") + update_credential.append({}) + if item.get("writeCommunity"): + update_credential[update_snmpv2cwrite_ptr] \ + .update({"writeCommunity": item.get("writeCommunity")}) + else: + self.msg = "writeCommunity is mandatory for updating \ + snmpV2cWrite " + str(have_snmpv2cwrite_ptr) + self.status = "failed" + return self + for i in range(1, 3): + if item.get(values[i]): + update_credential[update_snmpv2cwrite_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + update_credential[update_snmpv2cwrite_ptr].update({ + values[i]: self.have.get("globalCredential") + .get("snmpV2cWrite")[have_snmpv2cwrite_ptr].get(values[i]) + }) + update_snmpv2cwrite_ptr = update_snmpv2cwrite_ptr + 1 + have_snmpv2cwrite_ptr = have_snmpv2cwrite_ptr + 1 + + if CredentialDetails.get("httpsRead"): + httpsRead = CredentialDetails.get("httpsRead") + have_httpsread_ptr = 0 + create_httpsread_ptr = 0 + update_httpsread_ptr = 0 + values = ["password", "description", "username", "id", "port"] + have_httpsRead = self.have.get("globalCredential").get("httpsRead") + for item in httpsRead: + self.log(str(self.have.get("globalCredential"))) + if not have_httpsRead or have_httpsRead[have_httpsread_ptr] is None: + if want.get("want_create").get("httpsRead") is None: + want.get("want_create").update({"httpsRead": []}) + create_credential = want.get("want_create").get("httpsRead") + create_credential.append({}) + for i in range(0, 3): + if item.get(values[i]): + create_credential[create_httpsread_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + self.msg = values[i] + " is mandatory for creating \ + httpsRead " + str(have_httpsread_ptr) + self.status = "failed" + return self + if item.get("port"): + create_credential[create_httpsread_ptr] \ + .update({"port": item.get("port")}) + else: + create_credential[create_httpsread_ptr] \ + .update({"port": "443"}) + create_httpsread_ptr = create_httpsread_ptr + 1 + else: + if want.get("want_update").get("httpsRead") is None: + want.get("want_update").update({"httpsRead": []}) + update_credential = want.get("want_update").get("httpsRead") + update_credential.append({}) + if item.get("password"): + update_credential[update_httpsread_ptr] \ + .update({"password": item.get("password")}) + else: + self.msg = "password is mandatory for updating \ + httpsRead " + str(have_httpsread_ptr) + self.status = "failed" + return self + for i in range(1, 5): + if item.get(values[i]): + update_credential[update_httpsread_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + update_credential[update_httpsread_ptr].update({ + values[i]: self.have.get("globalCredential") + .get("httpsRead")[have_httpsread_ptr].get(values[i]) + }) + update_httpsread_ptr = update_httpsread_ptr + 1 + have_httpsread_ptr = have_httpsread_ptr + 1 + + if CredentialDetails.get("httpsWrite"): + httpsWrite = CredentialDetails.get("httpsWrite") + have_httpswrite_ptr = 0 + create_httpswrite_ptr = 0 + update_httpswrite_ptr = 0 + values = ["password", "description", "username", "id", "port"] + have_httpsWrite = self.have.get("globalCredential").get("httpsWrite") + for item in httpsWrite: + if not have_httpsWrite or have_httpsWrite[have_httpswrite_ptr] is None: + if want.get("want_create").get("httpsWrite") is None: + want.get("want_create").update({"httpsWrite": []}) + create_credential = want.get("want_create").get("httpsWrite") + create_credential.append({}) + for i in range(0, 3): + if item.get(values[i]): + create_credential[create_httpswrite_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + self.msg = values[i] + " is mandatory for creating \ + httpsWrite " + str(have_httpswrite_ptr) + self.status = "failed" + return self + if item.get("port"): + create_credential[create_httpswrite_ptr] \ + .update({"port": item.get("port")}) + else: + create_credential[create_httpswrite_ptr] \ + .update({"port": "443"}) + create_httpswrite_ptr = create_httpswrite_ptr + 1 + else: + if want.get("want_update").get("httpsWrite") is None: + want.get("want_update").update({"httpsWrite": []}) + update_credential = want.get("want_update").get("httpsWrite") + update_credential.append({}) + if item.get("password"): + update_credential[update_httpswrite_ptr] \ + .update({"password": item.get("password")}) + else: + self.msg = "password is mandatory for updating \ + httpsRead " + str(have_httpswrite_ptr) + self.status = "failed" + return self + for i in range(1, 5): + if item.get(values[i]): + update_credential[update_httpswrite_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + update_credential[update_httpswrite_ptr].update({ + values[i]: self.have.get("globalCredential") + .get("httpsWrite")[have_httpswrite_ptr].get(values[i]) + }) + update_httpswrite_ptr = update_httpswrite_ptr + 1 + have_httpswrite_ptr = have_httpswrite_ptr + 1 + + if CredentialDetails.get("snmpV3"): + snmpV3 = CredentialDetails.get("snmpV3") + have_snmpv3_ptr = 0 + create_snmpv3_ptr = 0 + update_snmpv3_ptr = 0 + values = ["description", "username", "id"] + have_snmpV3 = self.have.get("globalCredential").get("snmpV3") + for item in snmpV3: + if not have_snmpV3 or have_snmpV3[have_snmpv3_ptr] is None: + if want.get("want_create").get("snmpV3") is None: + want.get("want_create").update({"snmpV3": []}) + create_credential = want.get("want_create").get("snmpV3") + create_credential.append({}) + for i in range(0, 2): + if item.get(values[i]): + create_credential[create_snmpv3_ptr] \ + .update({values[i]: item.get(values[i])}) + else: + self.msg = values[i] + " is mandatory for creating \ + snmpV3 " + str(have_snmpv3_ptr) + self.status = "failed" + return self + if item.get("snmpMode"): + create_credential[create_snmpv3_ptr] \ + .update({"snmpMode": item.get("snmpMode")}) + 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"] + for auth in auths: + if item.get(auth): + create_credential[create_snmpv3_ptr] \ + .update({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" + self.status = "failed" + return self + elif create_credential[create_snmpv3_ptr].get("snmpMode") == "AUTHPRIV": + privs = ["privacyPassword", "privacyType"] + for priv in privs: + if item.get(priv): + create_credential[create_snmpv3_ptr] \ + .update({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" + self.status = "failed" + return self + elif create_credential[create_snmpv3_ptr].get("snmpMode") != "NOAUTHNOPRIV": + self.msg = "snmpMode in snmpV3 is not \ + ['AUTHPRIV', 'AUTHNOPRIV', 'NOAUTHNOPRIV']" + self.status = "failed" + return self + create_snmpv3_ptr = create_snmpv3_ptr + 1 + else: + if want.get("want_update").get("snmpV3") is None: + want.get("want_update").update({"snmpV3": []}) + update_credential = want.get("want_update").get("snmpV3") + update_credential.append({}) + for value in values: + if item.get(value): + update_credential[update_snmpv3_ptr] \ + .update({value: item.get(value)}) + else: + update_credential[update_snmpv3_ptr].update({ + value: self.have.get("globalCredential") + .get("snmpV3")[have_snmpv3_ptr].get(value) + }) + if item.get("snmpMode"): + update_credential[update_snmpv3_ptr] \ + .update({"snmpMode": item.get("snmpMode")}) + if update_credential[update_snmpv3_ptr].get("snmpMode") == "AUTHNOPRIV" or \ + update_credential[update_snmpv3_ptr].get("snmpMode") == "AUTHPRIV": + if item.get("authType"): + update_credential[update_snmpv3_ptr] \ + .update({"authType": item.get("authType")}) + elif self.have.get("globalCredential") \ + .get("snmpMode")[have_snmpv3_ptr].get("authType"): + update_credential[update_snmpv3_ptr].update({ + "authType": self.have.get("globalCredential") + .get("snmpMode")[have_snmpv3_ptr].get("authType") + }) + else: + self.msg = "authType is required for updating snmpV3 " + \ + str(have_snmpv3_ptr) + self.status = "failed" + return self + if item.get("authPassword"): + update_credential[update_snmpv3_ptr] \ + .update({"authPassword": item.get("authPassword")}) + else: + self.msg = "authPassword 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" + self.status = "failed" + return self + elif update_credential[update_snmpv3_ptr].get("snmpMode") == "AUTHPRIV": + if item.get("privacyType"): + update_credential[update_snmpv3_ptr] \ + .update({"privacyType": item.get("privacyType")}) + elif self.have.get("globalCredential") \ + .get("snmpMode")[have_snmpv3_ptr].get("privacyType"): + update_credential[update_snmpv3_ptr].update({ + "privacyType": self.have.get("globalCredential") + .get("snmpMode")[have_snmpv3_ptr].get("privacyType") + }) + else: + self.msg = "privacyType is required for updating snmpV3 " + \ + str(have_snmpv3_ptr) + self.status = "failed" + return self + if item.get("privacyPassword"): + update_credential[update_snmpv3_ptr] \ + .update({"privacyPassword": item.get("privacyPassword")}) + else: + self.msg = "privacyPassword 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" + self.status = "failed" + return self + update_snmpv3_ptr = update_snmpv3_ptr + 1 + have_snmpv3_ptr = have_snmpv3_ptr + 1 + self.want.update(want) + self.msg = "Collected the Global Credentials from the Cisco DNA Center" + self.status = "success" + return self + + def get_want_assign_credentials(self, AssignCredentials): + """ + Get the Credentials to be assigned to a site from the playbook. + Check this API using the check_return_status. + + Parameters: + AssignCredentials (dict) - Playbook details containing + credentials that need to be assigned to a site. + + Returns: + self - The current object with updated information of credentials + that need to be assigned to a site from the playbook. + """ + want = { + "assign_credentials": {} + } + siteName = AssignCredentials.get("siteName") + if not siteName: + self.msg = "siteName is required for AssignCredentials" + self.status = "failed" + return self + site_id = [] + for site_name in siteName: + siteId = self.get_site_id(site_name) + if not site_name: + self.msg = "siteName 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") + if cliId or cliDescription and cliUsername: + + # All CLI details from the Cisco DNA Center + cli_details = global_credentials.get("cliCredential") + if not cli_details: + self.msg = "No Global CLI Credential is available" + self.status = "failed" + return self + cliDetail = None + if cliId: + cliDetail = get_dict_result(cli_details, "id", cliId) + if not cliDetail: + self.msg = "CLI credential ID is invalid" + self.status = "failed" + return self + elif cliDescription and cliUsername: + for item in cli_details: + if item.get("description") == cliDescription and \ + item.get("username") == cliUsername: + cliDetail = item + if not cliDetail: + self.msg = "CLI credential username and description is invalid" + self.status = "failed" + return self + want.get("assign_credentials").update({"cliId": cliDetail.get("id")}) + + snmpV2cReadId = AssignCredentials.get("snmpV2ReadId") + snmpV2cReadDescription = AssignCredentials.get("snmpV2ReadDescription") + if snmpV2cReadId or snmpV2cReadDescription: + + # All snmpV2cRead details from the Cisco DNA Center + snmpV2cRead_details = global_credentials.get("snmpV2cRead") + if not snmpV2cRead_details: + self.msg = "No Global snmpV2cRead Credential is available" + self.status = "failed" + return self + snmpV2cReadDetail = None + if snmpV2cReadId: + snmpV2cReadDetail = get_dict_result(snmpV2cRead_details, "id", snmpV2cReadId) + if not snmpV2cReadDetail: + self.msg = "snmpV2cRead credential ID is invalid" + self.status = "failed" + return self + elif snmpV2cReadDescription: + for item in snmpV2cRead_details: + if item.get("description") == snmpV2cReadDescription: + snmpV2cReadDetail = item + if not snmpV2cReadDetail: + self.msg = "snmpV2cRead credential username and description is invalid" + self.status = "failed" + return self + want.get("assign_credentials").update({"snmpV2ReadId": snmpV2cReadDetail.get("id")}) + + snmpV2cWriteId = AssignCredentials.get("snmpV2WriteId") + snmpV2cWriteDescription = AssignCredentials.get("snmpV2WriteDescription") + if snmpV2cWriteId or snmpV2cWriteDescription: + + # All snmpV2cWrite details from the Cisco DNA Center + snmpV2cWrite_details = global_credentials.get("snmpV2cWrite") + if not snmpV2cWrite_details: + self.msg = "No Global snmpV2cWrite Credential is available" + self.status = "failed" + return self + snmpV2cWriteDetail = None + if snmpV2cWriteId: + snmpV2cWriteDetail = get_dict_result(snmpV2cWrite_details, "id", snmpV2cWriteId) + if not snmpV2cWriteDetail: + self.msg = "snmpV2cWrite credential ID is invalid" + self.status = "failed" + return self + elif snmpV2cWriteDescription: + for item in snmpV2cWrite_details: + if item.get("description") == snmpV2cWriteDescription: + snmpV2cWriteDetail = item + if not snmpV2cWriteDetail: + self.msg = "snmpV2cWrite credential username and description is invalid" + self.status = "failed" + return self + want.get("assign_credentials").update({"snmpV2WriteId": snmpV2cWriteDetail.get("id")}) + + httpReadId = AssignCredentials.get("httpRead") + httpReadDescription = AssignCredentials.get("httpReadDescription") + httpReadUsername = AssignCredentials.get("httpReadUsername") + if httpReadId or httpReadDescription and httpReadUsername: + + # All httpRead details from the Cisco DNA Center + httpRead_details = global_credentials.get("httpsRead") + if not httpRead_details: + self.msg = "No Global httpRead Credential is available" + self.status = "failed" + return self + httpReadDetail = None + if httpReadId: + httpReadDetail = get_dict_result(httpRead_details, "id", httpReadId) + if not httpReadDetail: + self.msg = "httpRead credential ID is invalid" + self.status = "failed" + return self + elif httpReadDescription and httpReadUsername: + for item in httpRead_details: + if item.get("description") == httpReadDescription and \ + item.get("username") == httpReadUsername: + httpReadDetail = item + if not httpReadDetail: + self.msg = "httpRead credential description and username is invalid" + self.status = "failed" + return self + want.get("assign_credentials").update({"httpRead": httpReadDetail.get("id")}) + + httpWriteId = AssignCredentials.get("httpWrite") + httpWriteDescription = AssignCredentials.get("httpWriteDescription") + httpWriteUsername = AssignCredentials.get("httpWriteUsername") + if httpWriteId or httpWriteDescription and httpWriteUsername: + + # All httpWrite details from the Cisco DNA Center + httpWrite_details = global_credentials.get("httpsWrite") + if not httpWrite_details: + self.msg = "No Global httpWrite Credential is available" + self.status = "failed" + return self + httpWriteDetail = None + if httpWriteId: + httpWriteDetail = get_dict_result(httpWrite_details, "id", httpWriteId) + if not httpWriteDetail: + self.msg = "httpWrite credential ID is invalid" + self.status = "failed" + return self + elif httpWriteDescription and httpWriteUsername: + for item in httpWrite_details: + if item.get("description") == httpWriteDescription and \ + item.get("username") == httpWriteUsername: + httpWriteDetail = item + if not httpWriteDetail: + self.msg = "httpWrite credential description and username is invalid" + self.status = "failed" + return self + want.get("assign_credentials").update({"httpWrite": httpWriteDetail.get("id")}) + + snmpV3Id = AssignCredentials.get("snmpV3Id") + snmpV3Description = AssignCredentials.get("snmpV3Description") + if snmpV3Id or snmpV3Description: + + # All snmpV3 details from the Cisco DNA Center + snmpV3_details = global_credentials.get("snmpV3") + if not snmpV3_details: + self.msg = "No Global snmpV3 Credential is available" + self.status = "failed" + return self + snmpV3Detail = None + if snmpV3Id: + snmpV3Detail = get_dict_result(snmpV3_details, "id", snmpV3Id) + if not snmpV3Detail: + self.msg = "snmpV3 credential ID is invalid" + self.status = "failed" + return self + elif snmpV3Description: + for item in snmpV3_details: + if item.get("description") == snmpV3Description: + snmpV3Detail = item + if not snmpV3Detail: + self.msg = "snmpV2cWrite credential username and description is invalid" + self.status = "failed" + return self + want.get("assign_credentials").update({"snmpV3Id": snmpV3Detail.get("id")}) + self.log("Assign Credentials to Site playbook values " + str(want)) + self.want.update(want) + self.msg = "Collected the Credentials needed to be assigned from the Cisco DNA Center" + self.status = "success" + return self + + def get_want(self, config): + """ + Get the current Global Device Credentials and Device + Credentials assigned to a site form the playbook. + + Parameters: + config (dict) - Playbook details containing Global Device + Credentials configurations and Device Credentials should + be assigned to a site. + + Returns: + self - The current object with updated information of Global + Device Credentials and Device Credentials assigned to a site. + """ + + if config.get("GlobalCredentialDetails"): + CredentialDetails = config.get("GlobalCredentialDetails") + self.get_want_device_credentials(CredentialDetails).check_return_status() + + if config.get("AssignCredentialsToSite"): + AssignCredentials = config.get("AssignCredentialsToSite") + self.get_want_assign_credentials(AssignCredentials).check_return_status() + + self.log("User details from the playbook " + str(self.want)) + self.msg = "Successfully retrieved details from the playbook" + self.status = "success" + return self + + def create_device_credentials(self): + """ + Create Global Device Credential to the Cisco DNA + Center based on the provided playbook details. + Check the return value of the API with check_return_status(). + + Parameters: + None + + Returns: + self + """ + + result_global_credential = self.result.get("response")[0].get("globalCredential") + want_create = self.want.get("want_create") + if not want_create: + result_global_credential.update({ + "No Creation": { + "response": "No Response", + "msg": "No Creation is available" + } + }) + return self + + credential_params = want_create + self.log("Create Global Credential API input - " + str(credential_params)) + response = self.dnac._exec( + family="discovery", + function='create_global_credentials_v2', + params=credential_params, + ) + self.log(str(response)) + if not response: + self.msg = "response is empty" + self.status = "exited" + return self + + if not isinstance(response, dict): + self.msg = "response is not a dictionary" + self.status = "exited" + return self + + task_id = response.get("response").get("taskId") + while True: + task_details = self.get_task_details(task_id) + self.log(str(task_details)) + if task_details.get("isError") is True: + self.msg = str(task_details.get("progress")) + self.status = "failed" + return self + elif task_details.get("isError") is False: + self.result['changed'] = True + break + self.log("Global Credential Created Successfully") + result_global_credential.update({ + "Creation": { + "response": credential_params, + "msg": "Global Credential Created Successfully" + } + }) + self.msg = "Global Device Credential Created Successfully" + self.status = "success" + return self + + def update_device_credentials(self): + """ + Update Device Credential to the Cisco DNA Center based on the provided playbook details. + Check the return value of the API with check_return_status(). + + Parameters: + None + + Returns: + self + """ + + result_global_credential = self.result.get("response")[0].get("globalCredential") + want_update = self.want.get("want_update") + if not want_update: + result_global_credential.update({ + "No Updation": { + "response": "No Response", + "msg": "No Updation is available" + } + }) + self.msg = "No Updation is available" + self.status = "success" + return self + i = 0 + flag = True + values = ["cliCredential", "snmpV2cRead", "snmpV2cWrite", + "httpsRead", "httpsWrite", "snmpV3"] + final_response = [] + self.log(str(want_update)) + while flag: + flag = False + credential_params = {} + for value in values: + if want_update.get(value) and i < len(want_update.get(value)): + flag = True + credential_params.update({value: want_update.get(value)[i]}) + i = i + 1 + if credential_params: + final_response.append(credential_params) + response = self.dnac._exec( + family="discovery", + function='update_global_credentials_v2', + params=credential_params, + ) + self.log(str(response)) + if not response: + self.msg = "response is empty" + self.status = "exited" + return self + + if not isinstance(response, dict): + self.msg = "response is not a dictionary" + self.status = "exited" + return self + + task_id = response.get("response").get("taskId") + while True: + task_details = self.get_task_details(task_id) + self.log(str(task_details)) + if task_details.get("isError") is True: + self.msg = str(task_details.get("progress")) + self.status = "failed" + return self + elif task_details.get("isError") is False: + self.result['changed'] = True + break + self.log("Update Device Credential API input - " + str(final_response)) + self.log("Global Device Credential Updated Successfully") + result_global_credential.update({ + "Updation": { + "response": final_response, + "msg": "Global Device Credential Updated Successfully" + } + }) + self.msg = "Global Device Credential Updated Successfully" + self.status = "success" + return self + + def assign_credentials_to_site(self): + """ + Assign Global Device Credential to the Cisco DNA + Center based on the provided playbook details. + Check the return value of the API with check_return_status(). + + Parameters: + None + + Returns: + self + """ + + result_assign_credential = self.result.get("response")[0].get("assignCredential") + credential_params = self.want.get("assign_credentials") + final_response = [] + self.log("Assign Device Credential to site API input - " + str(credential_params)) + if not credential_params: + result_assign_credential.update({ + "No Assign Credentials": { + "response": "No Response", + "msg": "No Assignment is available" + } + }) + self.msg = "No Assignment is available" + self.status = "success" + return self + for site_id in self.want.get("site_id"): + credential_params.update({"site_id": site_id}) + final_response.append(credential_params) + response = self.dnac._exec( + family="network_settings", + function='assign_device_credential_to_site_v2', + params=credential_params, + ) + self.log(str(response)) + if not response: + self.msg = "response is empty" + self.status = "failed" + return self + + if not isinstance(response, dict): + self.msg = "response is not a dictionary" + self.status = "failed" + return self + + task_id = response.get("response").get("taskId") + while True: + task_details = self.get_task_details(task_id) + self.log(str(task_details)) + if task_details.get("isError") is True: + self.msg = str(task_details.get("progress")) + self.status = "failed" + return self + elif task_details.get("isError") is False: + self.result['changed'] = True + break + + self.log("Device Credential Assigned to site is Successfully") + result_assign_credential.update({ + "Assign Credentials": { + "response": final_response, + "msg": "Device Credential Assigned to a site is Successfully" + } + }) + self.msg = "Global Credential is assigned Successfully" + self.status = "success" + return self + + def get_diff_merged(self, config): + """ + Update or Create Global Device Credential and assign device + credential to a site in Cisco DNA Center based on the playbook provided. + + Parameters: + config (list of dict) - Playbook details containing Global + Device Credential and assign credentials to a site information. + + Returns: + self + """ + + if config.get("GlobalCredentialDetails") is not None: + self.create_device_credentials().check_return_status() + + if config.get("GlobalCredentialDetails") is not None: + self.update_device_credentials().check_return_status() + + if config.get("AssignCredentialsToSite") is not None: + self.assign_credentials_to_site().check_return_status() + + return self + + def delete_device_credential(self, config): + """ + Delete Global Device Credential in Cisco DNA Center based on the playbook details. + Check the return value of the API with check_return_status(). + + Parameters: + None + + Returns: + None + """ + + result_global_credential = self.result.get("response")[0].get("globalCredential") + have_values = self.have.get("globalCredential") + final_response = {} + self.log(str(have_values)) + for item in have_values: + config_itr = 0 + 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 + _id = have_values.get(item)[config_itr].get("id") + response = self.dnac._exec( + family="discovery", + function="delete_global_credential_v2", + params={"id": _id}, + ) + self.log(str(response)) + if not response: + self.msg = "response is empty" + self.status = "exited" + return self + + if not isinstance(response, dict): + self.msg = "response is not a dictionary" + self.status = "exited" + return self + + task_id = response.get("response").get("taskId") + while True: + task_details = self.get_task_details(task_id) + if task_details.get("isError") is True: + self.msg = str(task_details.get("progress")) + self.status = "failed" + return self + elif task_details.get("isError") is False: + self.result['changed'] = True + break + final_response.get(item).append(_id) + config_itr = config_itr + 1 + + self.log("Delete Device Credential API input - " + str(final_response)) + self.log("Global Device Credential Deleted Successfully") + result_global_credential.update({ + "Deletion": { + "response": final_response, + "msg": "Global Device Credentials Deleted Successfully" + } + }) + self.msg = "Global Device Credentials Updated Successfully" + self.status = "success" + return self + + def get_diff_deleted(self, config): + """ + Delete Global Device Credential in Cisco DNA Center based on the playbook details. + + Parameters: + None + + Returns: + None + """ + + if config.get("GlobalCredentialDetails") is not None: + self.delete_device_credential(config).check_return_status() + + return self + + def reset_values(self): + """ + Reset all neccessary attributes to default values + + Parameters: + None + + Returns: + None + """ + + self.have.clear() + self.want.clear() + return + + +def main(): + """main entry point for module execution""" + + # Define the specification for module arguments + element_spec = { + "dnac_host": {"type": 'str', "required": True}, + "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}, + "config": {"type": 'list', "required": True, "elements": 'dict'}, + "state": {"default": 'merged', "choices": ['merged', 'deleted']}, + "validate_response_schema": {"type": 'bool', "default": True}, + } + + # Create an AnsibleModule object with argument specifications + module = AnsibleModule(argument_spec=element_spec, supports_check_mode=False) + dnac_credential = DnacCredential(module) + state = dnac_credential.params.get("state") + if state not in dnac_credential.supported_states: + dnac_credential.status = "invalid" + dnac_credential.msg = "State {0} is invalid".format(state) + dnac_credential.check_return_status() + + dnac_credential.validate_input().check_return_status() + + for config in dnac_credential.config: + dnac_credential.reset_values() + dnac_credential.get_have(config).check_return_status() + if state != "deleted": + dnac_credential.get_want(config).check_return_status() + dnac_credential.get_diff_state_apply[state](config).check_return_status() + + module.exit_json(**dnac_credential.result) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index b00852247e..4c1f406fd3 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -26,7 +26,7 @@ Madhan Sankaranarayanan (@madhansansel) options: state: - description: The state of DNAC after module completion. + description: The state of Cisco DNA Center after module completion. type: str choices: [ merged, deleted ] default: merged @@ -245,7 +245,7 @@ description: Network V2's snmpServer. suboptions: configureDnacIP: - description: Configuration DNAC IP for SNMP Server (eg true). + description: Configuration Cisco DNA Center IP for SNMP Server (eg true). type: bool ipAddresses: description: IP Address for SNMP Server (eg 4.4.4.1). @@ -256,7 +256,7 @@ description: Network V2's syslogServer. suboptions: configureDnacIP: - description: Configuration DNAC IP for syslog server (eg true). + description: Configuration Cisco DNA Center IP for syslog server (eg true). type: bool ipAddresses: description: IP Address for syslog server (eg 4.4.4.4). @@ -364,7 +364,7 @@ RETURN = r""" # Case_1: Successful creation/updation/deletion of global pool response_1: - description: A dictionary or list with the response returned by the Cisco DNAC Python SDK + description: A dictionary or list with the response returned by the Cisco DNA Center Python SDK returned: always type: dict sample: > @@ -376,7 +376,7 @@ # Case_2: Successful creation/updation/deletion of reserve pool response_2: - description: A dictionary or list with the response returned by the Cisco DNAC Python SDK + description: A dictionary or list with the response returned by the Cisco DNA Center Python SDK returned: always type: dict sample: > @@ -388,7 +388,7 @@ # Case_3: Successful creation/updation of network response_3: - description: A dictionary or list with the response returned by the Cisco DNAC Python SDK + description: A dictionary or list with the response returned by the Cisco DNA Center Python SDK returned: always type: dict sample: > @@ -555,15 +555,15 @@ def requires_update(self, have, want, obj_params): current information wih the requested information. This method compares the current global pool, reserve pool, - or network details from DNAC with the user-provided details + or network details from Cisco DNA Center with the user-provided details from the playbook, using a specified schema for comparison. Parameters: - have (dict) - Current information from the DNAC + 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 - DNAC parameters (dnac_param) correspond to + Cisco DNA Center parameters (dnac_param) correspond to the user-provided parameters (ansible_param). Returns: @@ -615,14 +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 DNAC + 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 Returns: dict or None - Processed Global Pool data in a format suitable - for DNAC configuration, or None if pool_info is empty. + for Cisco DNA Center configuration, or None if pool_info is empty. """ if not pool_info: @@ -658,14 +658,15 @@ 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 DNAC + Process Reserved Pool parameters from playbook data + for Reserved Pool configuration in Cisco DNA Center Parameters: 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 DNAC config + in the format suitable for the Cisco DNA Center config """ reserve_pool = { @@ -731,14 +732,16 @@ def get_reserve_pool_params(self, pool_info): def get_network_params(self, site_id): """ - Process the Network parameters from the playbook for Network configuration in DNAC + Process the Network parameters from the playbook + for Network configuration in Cisco DNA Center Parameters: site_id (str) - The Site ID for which network parameters are requested Returns: - dict or None: Processed Network data in a format suitable for DNAC 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( @@ -767,7 +770,7 @@ def get_network_params(self, site_id): clientAndEndpoint_aaa_pan = \ get_dict_result(all_network_details, "key", "aaa.server.pan.endpoint") - # Prepare the network details for DNAC configuration + # Prepare the network details for Cisco DNA Center configuration network_details = { "settings": { "snmpServer": { @@ -930,7 +933,7 @@ def reserve_pool_exists(self, name, site_name): def get_have_global_pool(self, config): """ Get the current Global Pool information from - DNAC based on the provided playbook details. + Cisco DNA Center based on the provided playbook details. check this API using check_return_status. Parameters: @@ -979,13 +982,13 @@ def get_have_global_pool(self, config): self.log("pool Exists: " + str(global_pool.get("exists")) + "\n Current Site: " + str(global_pool.get("details"))) self.have.update({"globalPool": global_pool}) - self.msg = "Collecting the global pool details from the DNAC" + self.msg = "Collecting the global pool details from the Cisco DNA Center" self.status = "success" return self def get_have_reserve_pool(self, config): """ - Get the current Reserved Pool information from DNAC + Get the current Reserved Pool information from Cisco DNA Center based on the provided playbook details. Check this API using check_return_status @@ -1015,7 +1018,8 @@ def get_have_reserve_pool(self, config): self.status = "failed" return self - # Check if the Reserved Pool exists in DNAC based on the provided name and site name + # Check if the Reserved Pool exists in Cisco DNA Center + # based on the provided name and site name reserve_pool = self.reserve_pool_exists(name, site_name) if not reserve_pool.get("success"): return self.check_return_status() @@ -1030,7 +1034,7 @@ def get_have_reserve_pool(self, config): if not reserve_pool.get("success"): return self.check_return_status() - # If the previous name doesn't exist in DNAC, return with error + # 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.status = "failed" @@ -1049,13 +1053,14 @@ def get_have_reserve_pool(self, config): self.log(str(reserve_pool)) self.have.update({"reservePool": reserve_pool}) - self.msg = "Collecting the reserve pool details from the DNAC" + self.msg = "Collecting the reserve pool details from the Cisco DNA Center" self.status = "success" return self def get_have_network(self, config): """ - Get the current Network details from DNAC based on the provided playbook details. + Get the current Network details from Cisco DNA + Center based on the provided playbook details. Parameters: config (dict) - Playbook details containing Network Management configuration. @@ -1078,15 +1083,15 @@ def get_have_network(self, config): network["site_id"] = site_id network["net_details"] = self.get_network_params(site_id) - self.log("Network Details from the DNAC " + str(network)) + self.log("Network Details from the Cisco DNA Center " + str(network)) self.have.update({"network": network}) - self.msg = "Collecting the network details from the DNAC" + self.msg = "Collecting the network details from the Cisco DNA Center" self.status = "success" return self def get_have(self, config): """ - Get the current Global Pool Reserved Pool and Network details from DNAC + Get the current Global Pool Reserved Pool and Network details from Cisco DNA Center Parameters: config (dict) - Playbook details containing Global Pool, @@ -1106,8 +1111,8 @@ def get_have(self, config): if config.get("NetworkManagementDetails") is not None: self.get_have_network(config).check_return_status() - self.log("Global Pool, Reserve Pool, Network Details in DNAC " + str(self.have)) - self.msg = "Successfully retrieved the details from the DNAC" + self.log("Global Pool, Reserve Pool, Network Details in Cisco DNA Center " + str(self.have)) + self.msg = "Successfully retrieved the details from the Cisco DNA Center" self.status = "success" return self @@ -1561,7 +1566,7 @@ def get_want(self, config): def update_global_pool(self, config): """ - Update/Create Global Pool in DNAC with fields provided in DNAC + Update/Create Global Pool in Cisco DNA Center with fields provided in playbook Parameters: config (list of dict) - Playbook details @@ -1599,7 +1604,7 @@ def update_global_pool(self, config): self.want.get("wantGlobal"), obj_params): self.log("Global pool doesn't requires an update") result_global_pool.get("response").get(name).update({ - "DNAC params": + "Cisco DNA Center params": self.have.get("globalPool").get("details").get("settings").get("ippool")[0] }) result_global_pool.get("response").get(name).update({ @@ -1643,8 +1648,8 @@ def update_global_pool(self, config): def update_reserve_pool(self, config): """ - Update or Create a Reserve Pool in DNAC based on the provided configuration. - This method checks if a reserve pool with the specified name exists in DNAC. + Update or Create a Reserve Pool in Cisco DNA Center based on the provided configuration. + This method checks if a reserve pool with the specified name exists in Cisco DNA Center. If it exists and requires an update, it updates the pool. If not, it creates a new pool. Parameters: @@ -1657,7 +1662,7 @@ def update_reserve_pool(self, config): name = config.get("ReservePoolDetails").get("name") result_reserve_pool = self.result.get("response")[1].get("reservePool") result_reserve_pool.get("response").update({name: {}}) - self.log("Reserve Pool DNAC Details " + + self.log("Reserve Pool Cisco DNA Center Details " + str(self.have.get("reservePool").get("details"))) self.log("Reserve Pool User Details " + str(self.want.get("wantReserve"))) @@ -1704,7 +1709,7 @@ def update_reserve_pool(self, config): self.want.get("wantReserve"), obj_params): self.log("Reserved ip subpool doesn't require an update") result_reserve_pool.get("response").get(name) \ - .update({"DNAC params": self.have.get("reservePool").get("details")}) + .update({"Cisco DNA Center params": self.have.get("reservePool").get("details")}) result_reserve_pool.get("response").get(name) \ .update({"Id": self.have.get("reservePool").get("id")}) result_reserve_pool.get("msg") \ @@ -1713,7 +1718,7 @@ def update_reserve_pool(self, config): self.log("Reserve ip pool requires an update") # Pool Exists - self.log("Reserved Ip Pool DNAC Details " + str(self.have.get("reservePool"))) + self.log("Reserved Ip Pool Cisco DNA Center Details " + str(self.have.get("reservePool"))) self.log("Reserved Ip Pool User Details" + str(self.want.get("wantReserve"))) reserve_params.update({"id": self.have.get("reservePool").get("id")}) response = self.dnac._exec( @@ -1730,7 +1735,8 @@ def update_reserve_pool(self, config): def update_network(self, config): """ - Update or create a network configuration in DNAC based on the provided playbook details. + Update or create a network configuration in Cisco DNA + Center based on the provided playbook details. Parameters: config (list of dict) - Playbook details containing Network Management information. @@ -1753,13 +1759,14 @@ def update_network(self, config): self.log("Network doesn't require an update") result_network.get("response").get(siteName).update({ - "DNAC params": self.have.get("network").get("net_details").get("settings") + "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"}) return self.log("Network requires update") - self.log("Network DNAC Details" + str(self.have.get("network"))) + self.log("Network Cisco DNA Center Details" + str(self.have.get("network"))) self.log("Network User Details" + str(self.want.get("wantNetwork"))) net_params = copy.deepcopy(self.want.get("wantNetwork")) @@ -1781,7 +1788,7 @@ def update_network(self, config): def get_diff_merged(self, config): """ Update or create Global Pool, Reserve Pool, and - Network configurations in DNAC based on the playbook details + Network configurations in Cisco DNA Center based on the playbook details Parameters: config (list of dict) - Playbook details containing @@ -1804,7 +1811,7 @@ def get_diff_merged(self, config): def delete_reserve_pool(self, name): """ - Delete a Reserve Pool by name in DNAC + Delete a Reserve Pool by name in Cisco DNA Center Parameters: name (str) - The name of the Reserve Pool to be deleted. @@ -1843,7 +1850,7 @@ def delete_reserve_pool(self, name): def delete_global_pool(self, name): """ - Delete a Global Pool by name in DNAC + Delete a Global Pool by name in Cisco DNA Center Parameters: name (str) - The name of the Global Pool to be deleted. @@ -1879,7 +1886,7 @@ def delete_global_pool(self, name): def get_diff_deleted(self, config): """ - Delete Reserve Pool and Global Pool in DNAC 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 From 92835d94a4f9c0760999029c63b5f0b1fac55a50 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Thu, 2 Nov 2023 00:39:39 +0530 Subject: [PATCH 11/32] Changes in the sanity ignore files --- tests/sanity/ignore-2.10.txt | 2 ++ tests/sanity/ignore-2.11.txt | 2 ++ tests/sanity/ignore-2.12.txt | 2 ++ tests/sanity/ignore-2.13.txt | 2 ++ tests/sanity/ignore-2.14.txt | 2 ++ tests/sanity/ignore-2.15.txt | 2 ++ tests/sanity/ignore-2.9.txt | 2 ++ 7 files changed, 14 insertions(+) diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index 91360148ff..0ab655e255 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -724,3 +724,5 @@ plugins/modules/pnp_intent.py import-2.7!skip # Python 2.7 is not supported by t plugins/modules/template_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/site_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/swim_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index 36f702f1b3..ca55095754 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -1075,3 +1075,5 @@ plugins/modules/pnp_intent.py import-2.6!skip # Python 2.6 is not supported by t plugins/modules/site_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/swim_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/template_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt index a6ce8c2866..354edbbd15 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -22,3 +22,5 @@ plugins/modules/pnp_intent.py import-2.7!skip # Python 2.7 is not supported by t plugins/modules/template_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/site_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/swim_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt index a0cfbc0671..ccbf1b9761 100644 --- a/tests/sanity/ignore-2.13.txt +++ b/tests/sanity/ignore-2.13.txt @@ -10,3 +10,5 @@ plugins/modules/pnp_intent.py import-2.7!skip # Python 2.7 is not supported by t plugins/modules/template_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/site_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/swim_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt index a0cfbc0671..ccbf1b9761 100644 --- a/tests/sanity/ignore-2.14.txt +++ b/tests/sanity/ignore-2.14.txt @@ -10,3 +10,5 @@ plugins/modules/pnp_intent.py import-2.7!skip # Python 2.7 is not supported by t plugins/modules/template_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/site_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/swim_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt index a0cfbc0671..ccbf1b9761 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -10,3 +10,5 @@ plugins/modules/pnp_intent.py import-2.7!skip # Python 2.7 is not supported by t plugins/modules/template_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/site_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/swim_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 91360148ff..0ab655e255 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -724,3 +724,5 @@ plugins/modules/pnp_intent.py import-2.7!skip # Python 2.7 is not supported by t plugins/modules/template_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/site_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/swim_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK From d65234dfedcba1e5698b74853c23b9066b9dec3a Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Thu, 2 Nov 2023 00:46:06 +0530 Subject: [PATCH 12/32] Changes in network settings intent module --- plugins/modules/network_settings_intent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index 4c1f406fd3..fa38578daa 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -1759,8 +1759,8 @@ def update_network(self, config): self.log("Network doesn't require an update") result_network.get("response").get(siteName).update({ - "Cisco DNA Center params": self.have.get("network") \ - .get("net_details").get("settings") + "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"}) return From f9ea7da97853414a1376b55b0b9432254786fc26 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Thu, 2 Nov 2023 00:59:19 +0530 Subject: [PATCH 13/32] Changes in sanity files --- tests/sanity/ignore-2.10.txt | 2 ++ tests/sanity/ignore-2.11.txt | 2 ++ tests/sanity/ignore-2.12.txt | 2 ++ tests/sanity/ignore-2.13.txt | 2 +- tests/sanity/ignore-2.14.txt | 2 +- tests/sanity/ignore-2.15.txt | 3 ++- tests/sanity/ignore-2.9.txt | 2 ++ 7 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index 0ab655e255..4c1b04f143 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -726,3 +726,5 @@ plugins/modules/site_intent.py import-2.7!skip # Python 2.7 is not supported by plugins/modules/swim_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index ca55095754..2ee339e701 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -1077,3 +1077,5 @@ plugins/modules/swim_intent.py import-2.6!skip # Python 2.6 is not supported by plugins/modules/template_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt index 354edbbd15..98bf9eed14 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -24,3 +24,5 @@ plugins/modules/site_intent.py import-2.7!skip # Python 2.7 is not supported by plugins/modules/swim_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt index ccbf1b9761..0df7244629 100644 --- a/tests/sanity/ignore-2.13.txt +++ b/tests/sanity/ignore-2.13.txt @@ -11,4 +11,4 @@ plugins/modules/template_intent.py import-2.7!skip # Python 2.7 is not supported plugins/modules/site_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/swim_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK -plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt index ccbf1b9761..0df7244629 100644 --- a/tests/sanity/ignore-2.14.txt +++ b/tests/sanity/ignore-2.14.txt @@ -11,4 +11,4 @@ plugins/modules/template_intent.py import-2.7!skip # Python 2.7 is not supported plugins/modules/site_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/swim_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK -plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt index ccbf1b9761..ad6f00c0b7 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -11,4 +11,5 @@ plugins/modules/template_intent.py import-2.7!skip # Python 2.7 is not supported plugins/modules/site_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/swim_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK -plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK + diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 0ab655e255..4c1b04f143 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -726,3 +726,5 @@ plugins/modules/site_intent.py import-2.7!skip # Python 2.7 is not supported by plugins/modules/swim_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/device_credential_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK From 8c72c839d33ab3a269d6f9f1af9343ebf805ed52 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Thu, 2 Nov 2023 01:07:03 +0530 Subject: [PATCH 14/32] Changes in the sanity files --- tests/sanity/ignore-2.15.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt index ad6f00c0b7..0df7244629 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -12,4 +12,3 @@ plugins/modules/site_intent.py import-2.7!skip # Python 2.7 is not supported by plugins/modules/swim_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/device_credential_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK - From 4e09a362c392d2fc8c20a387fd99b5bff20fcfd0 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 2 Nov 2023 14:59:20 +0530 Subject: [PATCH 15/32] Add code for Resync of list of devices with force flag and fix PR comments of adding different device types --- plugins/modules/inventory_intent.py | 157 +++++++++++++++++++--------- 1 file changed, 110 insertions(+), 47 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 22a45e6bb6..759f1ac26b 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -297,6 +297,22 @@ resync: false type: "THIRD_PARTY_DEVICE" +- name: Resync Device 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: + - ipAddress: string + resync: True + force_sync: False + - name: Delete Device by id cisco.dnac.inventory_intent: dnac_host: "{{dnac_host}}" @@ -391,7 +407,8 @@ def validate_input(self): 'snmpVersion': {'default': "v3", 'type': 'str'}, 'updateMgmtIPaddressList': {'type': 'list', 'elements': 'dict'}, 'userName': {'type': 'str'}, - 'resync': {'type': 'bool'} + 'resync': {'type': 'bool'}, + 'force_sync': {'type': 'bool'} } # Validate device params @@ -410,6 +427,7 @@ def validate_input(self): log(str(valid_temp)) self.msg = "Successfully validated input" self.status = "success" + return self def device_exists_in_dnac(self, want_device): @@ -449,32 +467,6 @@ def device_exists_in_dnac(self, want_device): return device_in_dnac - def get_device_id(self, device_ip): - """ - Get the unique device ID for a device with the specified management IP address in Cisco DNA Center. - Args: - device_ip (str): The management IP address of the device for which you want to retrieve the device ID. - Returns: - str: The unique device ID for the specified device. - Description: - Queries Cisco DNA Center to retrieve the unique device ID associated with a device having the specified - IP address. If the device is not found in Cisco DNA Center, it raises an exception. - """ - try: - response = self.dnac._exec( - family="devices", - function='get_device_list', - params={"managementIpAddress": device_ip}, - ) - except Exception as e: - log("An error occurred while fetching the device from Cisco DNA Center") - raise Exception("Error while fetching device from Cisco DNA Center") - - response = response.get("response")[0] - device_id = response.get("id") - - return device_id - def mandatory_parameter(self, config): """ Check for and validate mandatory parameters for adding network devices in Cisco DNA Center. @@ -487,23 +479,16 @@ 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"] + } + params_list = params_dict.get(device_type, []) mandatory_params_absent = [] - if device_type == "NETWORK_DEVICE": - params_list = ["enablePassword", "ipAddress", "password", "snmpUserName", "snmpAuthPassphrase", "snmpPrivPassphrase", "userName"] - - elif device_type == "COMPUTE_DEVICE": - params_list = ["ipAddress", "httpUserName", "httpPassword", "httpPort", "snmpUserName", "snmpAuthPassphrase", "snmpPrivPassphrase"] - - elif device_type == "MERAKI_DASHBOARD": - params_list = ["httpPassword"] - - elif device_type == "FIREPOWER_MANAGEMENT_SYSTEM": - params_list = ["ipAddress", "httpUserName", "httpPassword"] - - elif device_type == "THIRD_PARTY_DEVICE": - params_list = ["ipAddress", "snmpUserName", "snmpAuthPassphrase", "snmpPrivPassphrase"] - for param in params_list: if param not in config: mandatory_params_absent.append(param) @@ -594,11 +579,46 @@ def get_device_params(self, params): "serial_number": params.get("serialNumber"), "snmp_version": params.get("snmpVersion"), "type": params.get("type"), - "update_management_ip_list": params.get("updateMgmtIPaddressList") + "update_management_ip_list": params.get("updateMgmtIPaddressList"), + "force_sync": params.get("force_sync") } return device_param + def get_device_ids(self, device_ips): + """ + Get the list of unique device ID's for list of specified management IP addresses of devices in Cisco DNA Center. + Args: + 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 ID's. + Returns: + list: The list of unique device ID's for the specified devices. + Description: + Queries Cisco DNA Center to retrieve the unique device ID associated with a device having the specified + IP address. If the device is not found in Cisco DNA Center, it raises an exception. + """ + + device_ids = [] + for device_ip in device_ips: + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + params={"managementIpAddress": device_ip} + ) + + if response: + log(str(response)) + response = response.get("response")[0] + device_id = response["id"] + device_ids.append(device_id) + + except Exception as e: + log("An error occurred while fetching the device from Cisco DNA Center") + raise Exception("Error while fetching device from Cisco DNA Center") + + return device_ids + def get_want(self, config): """ Get all the device related information from playbook that is needed to be @@ -639,12 +659,55 @@ def get_diff_merged(self, config): device_added = False device_updated = False - # device_resynced = False devices_to_add = self.have["device_not_in_dnac"] - device_type = self.config[0].get('type') + device_type = self.config[0].get("type", "NETWORK_DEVICE") + device_resynced = self.config[0].get("device_resync", "False") self.result['log'] = [] + if device_resynced: + # Code for Resyncig of devices + device_ips = config.get("ipAddress") + 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, + ) + 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") and execution_details.get("failureReason"): + self.msg = "Device Resynced get failed because of {0}".format(execution_details.get("failureReason")) + self.status = "failed" + break + log("Device Resynced Successfully") + log("Resynced devices are :" + str(device_ips)) + msg = "Device " + str(device_ips) + " Resynced Successfully !!" + self.result['log'].append(msg) + + return self + + except Exception as e: + log("An error occurred while Resyncing device in Cisco DNA Center") + raise Exception("Error while Resyncing device in Cisco DNA Center") + if not devices_to_add: # Write code for device updation device_updated = True @@ -725,12 +788,12 @@ def get_diff_deleted(self, config): self.result['msg'].append(msg) continue - device_id = self.get_device_id(device_ip) + device_id = self.get_device_ids([device_ip]) try: response = self.dnac._exec( family="devices", function='delete_device_by_id', - params={"id": device_id}, + params={"id": device_id[0]}, ) if response and isinstance(response, dict): From 6af7fb535826e03198d86e914936c0c826640d78 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 2 Nov 2023 16:48:04 +0530 Subject: [PATCH 16/32] addressed PR review comments --- plugins/modules/inventory_intent.py | 59 +++++++++++++++-------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 759f1ac26b..b6bc83b63f 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -209,6 +209,7 @@ - existMgmtIpAddress: string newMgmtIpAddress: string userName: string + deviceResync: false - name: Add new Compute device in Inventory with full credentials.Inputs needed for Compute Device cisco.dnac.inventory_intent: @@ -235,7 +236,7 @@ snmpTimeout: 5 snmpUserName: string userName: string - resync: false + deviceResync: false type: "COMPUTE_DEVICE" - name: Add new Meraki device in Inventory with full credentials.Inputs needed for Meraki Device. @@ -251,7 +252,7 @@ state: merged config: - httpPassword: string - resync: false + deviceResync: false type: "MERAKI_DASHBOARD" - name: Add new Firepower Management device in Inventory with full credentials.Input needed to add Device. @@ -270,7 +271,7 @@ httpUserName: string httpPassword: string httpPort: string - resync: false + deviceResync: false type: "FIREPOWER_MANAGEMENT_SYSTEM" - name: Add new Third Party device in Inventory with full credentials.Input needed to add Device. @@ -294,7 +295,7 @@ snmpRetry: 3 snmpTimeout: 5 snmpUserName: string - resync: false + deviceResync: false type: "THIRD_PARTY_DEVICE" - name: Resync Device with IP Addresses @@ -310,8 +311,8 @@ state: merged config: - ipAddress: string - resync: True - force_sync: False + deviceResync: True + forceSync: False - name: Delete Device by id cisco.dnac.inventory_intent: @@ -407,8 +408,8 @@ def validate_input(self): 'snmpVersion': {'default': "v3", 'type': 'str'}, 'updateMgmtIPaddressList': {'type': 'list', 'elements': 'dict'}, 'userName': {'type': 'str'}, - 'resync': {'type': 'bool'}, - 'force_sync': {'type': 'bool'} + 'deviceResync': {'type': 'bool'}, + 'forceSync': {'type': 'bool'} } # Validate device params @@ -455,8 +456,8 @@ def device_exists_in_dnac(self, want_device): ) except Exception as e: - log("An error occurred while fetching the device from Cisco DNA Center") - raise Exception("Error while fetching device from Cisco DNA Center") + self.log("An error occurred while fetching the device from Cisco DNA Center") + raise Exception(f"'Error while fetching device from Cisco DNA Center' - {str(e)}") if response: log(str(response)) @@ -580,19 +581,19 @@ def get_device_params(self, params): "snmp_version": params.get("snmpVersion"), "type": params.get("type"), "update_management_ip_list": params.get("updateMgmtIPaddressList"), - "force_sync": params.get("force_sync") + "force_sync": params.get("forceSync") } return device_param def get_device_ids(self, device_ips): """ - Get the list of unique device ID's for list of specified management IP addresses of devices in Cisco DNA Center. + Get the list of unique device IDs for list of specified management IP addresses of devices in Cisco DNA Center. Args: 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 ID's. + device_ips (list): The management IP addresses of devices for which you want to retrieve the device IDs. Returns: - list: The list of unique device ID's for the specified devices. + list: The list of unique device IDs for the specified devices. Description: Queries Cisco DNA Center to retrieve the unique device ID associated with a device having the specified IP address. If the device is not found in Cisco DNA Center, it raises an exception. @@ -608,14 +609,14 @@ def get_device_ids(self, device_ips): ) if response: - log(str(response)) + self.log(str(response)) response = response.get("response")[0] device_id = response["id"] device_ids.append(device_id) except Exception as e: - log("An error occurred while fetching the device from Cisco DNA Center") - raise Exception("Error while fetching device from Cisco DNA Center") + self.log("An error occurred while fetching the device from Cisco DNA Center") + raise Exception(f"'Error while fetching device from Cisco DNA Center' - {str(e)}") return device_ids @@ -662,15 +663,15 @@ 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_resynced = self.config[0].get("deviceResync", "False") self.result['log'] = [] if device_resynced: - # Code for Resyncig of devices + # Code for triggers the resync operation using the retrieved device IDs and force sync parameter. device_ips = config.get("ipAddress") device_ids = self.get_device_ids(device_ips) try: - force_sync = self.config[0].get("force_sync", "False") + force_sync = self.config[0].get("forceSync", "False") resync_param_dict = { 'payload': device_ids, 'force_sync': force_sync @@ -681,7 +682,7 @@ def get_diff_merged(self, config): op_modifies=True, params=resync_param_dict, ) - log(str(response)) + self.log(str(response)) if response and isinstance(response, dict): task_id = response.get('response').get('taskId') @@ -697,16 +698,16 @@ def get_diff_merged(self, config): self.msg = "Device Resynced get failed because of {0}".format(execution_details.get("failureReason")) self.status = "failed" break - log("Device Resynced Successfully") - log("Resynced devices are :" + str(device_ips)) + self.log("Device Resynced Successfully") + self.log("Resynced devices are :" + str(device_ips)) msg = "Device " + str(device_ips) + " Resynced Successfully !!" self.result['log'].append(msg) return self except Exception as e: - log("An error occurred while Resyncing device in Cisco DNA Center") - raise Exception("Error while Resyncing device in Cisco DNA Center") + self.log("An error occurred while Resyncing device in Cisco DNA Center") + raise Exception(f"'Error while Resyncing device in Cisco DNA Center' - {str(e)}") if not devices_to_add: # Write code for device updation @@ -758,8 +759,8 @@ def get_diff_merged(self, config): self.result['log'].append(msg) except Exception as e: - log("An error occurred while Adding device in Cisco DNA Center") - raise Exception("Error while Adding device in Cisco DNA Center") + self.log("An error occurred while Adding device in Cisco DNA Center") + raise Exception(f"'Error while Adding device in Cisco DNA Center' - {str(e)}") return self @@ -813,8 +814,8 @@ def get_diff_deleted(self, config): break except Exception as e: - log("An error occurred while Deleting the device from Cisco DNA Center") - raise Exception("Error while Deleting device from Cisco DNA Center") + self.log("An error occurred while Deleting the device from Cisco DNA Center") + raise Exception(f"'Error while Deleting device from Cisco DNA Center' - {str(e)}") return self From ba883a9421ca46bb6a661deb162069361381a018 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Thu, 2 Nov 2023 17:33:50 +0530 Subject: [PATCH 17/32] replace f string to format in exception handling for supporting all python versions --- plugins/modules/inventory_intent.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index b6bc83b63f..5dc984d720 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -457,7 +457,8 @@ def device_exists_in_dnac(self, want_device): except Exception as e: self.log("An error occurred while fetching the device from Cisco DNA Center") - raise Exception(f"'Error while fetching device from Cisco DNA Center' - {str(e)}") + error_message = "Error while fetching device from Cisco DNA Center - {0}".format(str(e)) + raise Exception(error_message) if response: log(str(response)) @@ -616,7 +617,8 @@ def get_device_ids(self, device_ips): except Exception as e: self.log("An error occurred while fetching the device from Cisco DNA Center") - raise Exception(f"'Error while fetching device from Cisco DNA Center' - {str(e)}") + error_message = "Error while fetching device from Cisco DNA Center - {0}".format(str(e)) + raise Exception(error_message) return device_ids @@ -707,7 +709,8 @@ def get_diff_merged(self, config): except Exception as e: self.log("An error occurred while Resyncing device in Cisco DNA Center") - raise Exception(f"'Error while Resyncing device in Cisco DNA Center' - {str(e)}") + error_message = "Error while Resyncing device in Cisco DNA Center - {0}".format(str(e)) + raise Exception(error_message) if not devices_to_add: # Write code for device updation @@ -760,7 +763,8 @@ def get_diff_merged(self, config): except Exception as e: self.log("An error occurred while Adding device in Cisco DNA Center") - raise Exception(f"'Error while Adding device in Cisco DNA Center' - {str(e)}") + error_message = "Error while Adding device in Cisco DNA Center - {0}".format(str(e)) + raise Exception(error_message) return self @@ -815,7 +819,8 @@ def get_diff_deleted(self, config): except Exception as e: self.log("An error occurred while Deleting the device from Cisco DNA Center") - raise Exception(f"'Error while Deleting device from Cisco DNA Center' - {str(e)}") + error_message = "Error while Deleting device from Cisco DNA Center - {0}".format(str(e)) + raise Exception(error_message) return self From 740e56d511006673cc74f75bf8bc7e8e95aadeb7 Mon Sep 17 00:00:00 2001 From: Abhishek-121 Date: Fri, 3 Nov 2023 10:03:31 +0530 Subject: [PATCH 18/32] used error_message in log of exception --- plugins/modules/inventory_intent.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index 5dc984d720..b6ad406718 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -62,7 +62,7 @@ description: Id path parameter. Device ID.Required for Deleting Device. type: str ipAddress: - description: Network Device's ipAddress.Required for Adding/Deleting Device except Meraki Devices. + description: Network Device's ipAddress.Required for Adding/Deleting/Resyncing Device except Meraki Devices. elements: str type: list merakiOrgId: @@ -456,8 +456,8 @@ def device_exists_in_dnac(self, want_device): ) except Exception as e: - self.log("An error occurred while fetching the device from Cisco DNA Center") error_message = "Error while fetching device from Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) raise Exception(error_message) if response: @@ -616,8 +616,8 @@ def get_device_ids(self, device_ips): device_ids.append(device_id) except Exception as e: - self.log("An error occurred while fetching the device from Cisco DNA Center") error_message = "Error while fetching device from Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) raise Exception(error_message) return device_ids @@ -708,8 +708,8 @@ def get_diff_merged(self, config): return self except Exception as e: - self.log("An error occurred while Resyncing device in Cisco DNA Center") error_message = "Error while Resyncing device in Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) raise Exception(error_message) if not devices_to_add: @@ -762,8 +762,8 @@ def get_diff_merged(self, config): self.result['log'].append(msg) except Exception as e: - self.log("An error occurred while Adding device in Cisco DNA Center") error_message = "Error while Adding device in Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) raise Exception(error_message) return self @@ -818,8 +818,8 @@ def get_diff_deleted(self, config): break except Exception as e: - self.log("An error occurred while Deleting the device from Cisco DNA Center") 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 5d78afbc9f3cb043ff1067a0e9190adab9feee1c Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 3 Nov 2023 15:36:23 +0530 Subject: [PATCH 19/32] Addressed PR comment --- plugins/modules/device_credential_intent.py | 1066 ++++++++++--------- 1 file changed, 581 insertions(+), 485 deletions(-) diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index e75ea79ccb..0f6783ba7d 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -47,25 +47,32 @@ elements: dict suboptions: description: - description: Description. + description: Description. Required for creating the credential. type: str enablePassword: - description: Enable Password. + description: + - cliCredential credential Enable Password. + - Password cannot contain spaces or angle brackets (< >) type: str id: - description: Id. + description: Credential Id. Use this for updating the device credential. type: str password: - description: Password. + description: + - cliCredential credential Password. + - Required for creating/updating the credential. + - Password cannot contain spaces or angle brackets (< >). type: str username: - description: Username. + description: + - cliCredential credential Username. + - Username cannot contain spaces or angle brackets (< >). type: str old_description: - description: Old Description + description: Old Description. Use this for updating the description/Username. type: str old_username: - description: Old Username + description: Old Username. Use this for updating the description/Username. type: str httpsRead: description: Global Credential V2's httpsRead. @@ -73,25 +80,30 @@ elements: dict suboptions: id: - description: Id. + description: Credential Id. Use this for updating the device credential. type: str name: - description: Name. + description: Name. Required for creating the credential. type: str password: - description: Password. + description: + - httpsRead credential Password. + - Required for creating/updating the credential. + - Password cannot contain spaces or angle brackets (< >). type: str port: - description: Port. + description: Port. Default port is 443. type: int username: - description: Username. + description: + - httpsRead credential Username. + - Username cannot contain spaces or angle brackets (< >). type: str old_description: - description: Old Description + description: Old Description. Use this for updating the description/Username. type: str old_username: - description: Old Username + description: Old Username. Use this for updating the description/Username. type: str httpsWrite: description: Global Credential V2's httpsWrite. @@ -99,25 +111,30 @@ elements: dict suboptions: id: - description: Id. + description: Credential Id. Use this for updating the device credential. type: str name: - description: Name. + description: Name. Required for creating the credential. type: str password: - description: Password. + description: + - httpsWrite credential Password. + - Required for creating/updating the credential. + - Password cannot contain spaces or angle brackets (< >). type: str port: - description: Port. + description: Port. Default port is 443. type: int username: - description: Username. + description: + - httpsWrite credential Username. + - Username cannot contain spaces or angle brackets (< >). type: str old_description: - description: Old Description + description: Old Description. Use this for updating the description/Username. type: str old_username: - description: Old Username + description: Old Username. Use this for updating the description/Username. type: str snmpV2cRead: description: Global Credential V2's snmpV2cRead. @@ -125,16 +142,18 @@ elements: dict suboptions: description: - description: Description. + description: Description. Required for creating the credential. type: str id: - description: Id. + description: Credential Id. Use this for updating the device credential. type: str readCommunity: - description: Read Community. + description: + - snmpV2cRead Read Community. + - Password cannot contain spaces or angle brackets (< >). type: str old_description: - description: Old Description. + description: Old Description. Use this for updating the description. type: str snmpV2cWrite: description: Global Credential V2's snmpV2cWrite. @@ -142,16 +161,18 @@ elements: dict suboptions: description: - description: Description. + description: Description. Required for creating the credential. type: str id: - description: Id. + description: Credential Id. Use this for updating the device credential. type: str writeCommunity: - description: Write Community. + description: + - snmpV2cWrite Write Community. + - Password cannot contain spaces or angle brackets (< >). type: str old_description: - description: Old Description. + description: Old Description. Use this for updating the description. type: str snmpV3: description: Global Credential V2's snmpV3. @@ -159,28 +180,41 @@ elements: dict suboptions: authPassword: - description: Auth Password. + description: + - snmpV3 Auth Password. + - Password must contain minimum 8 characters. + - Password cannot contain spaces or angle brackets (< >). type: str authType: - description: Auth Type. + description: Auth Type. ["SHA", "MD5"]. type: str description: - description: Description. + description: + - snmpV3 Description. + - Should be unique from other snmpV3 credentials. type: str id: - description: Id. + description: Credential Id. Use this for updating the device credential. type: str privacyPassword: - description: Privacy Password. + description: + - snmpV3 Privacy Password. + - Password must contain minimum 8 characters. + - Password cannot contain spaces or angle brackets (< >). type: str privacyType: - description: Privacy Type. + description: Privacy Type. ["AES128", "AES192", "AES256"]. type: str snmpMode: - description: Snmp Mode. + description: Snmp Mode. ["AUTHPRIV", "AUTHNOPRIV", "NOAUTHNOPRIV"]. type: str username: - description: Username. + description: + - snmpV3 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: description: Assign Device Credentials to Site. @@ -193,7 +227,7 @@ description: CLI Credential Username. type: str cliId: - description: CLI Credential Id. + description: CLI Credential Id. Use (Description, Username) or Id. type: str httpReadDescription: description: HTTP(S) Read Credential Description. @@ -202,7 +236,7 @@ description: HTTP(S) Read Credential Username. type: str httpRead: - description: HTTP(S) Read Credential Id. + description: HTTP(S) Read Credential Id. Use (Description, Username) or Id. type: str httpWriteDescription: description: HTTP(S) Write Credential Description. @@ -211,7 +245,7 @@ description: HTTP(S) Write Credential Username. type: str httpWrite: - description: HTTP(S) Write Credential Id. + description: HTTP(S) Write Credential Id. Use (Description, Username) or Id. type: str siteName: description: Site Name to assign credential. @@ -221,19 +255,19 @@ description: SNMPv2c Read Credential Description. type: str snmpV2ReadId: - description: SNMPv2c Read Credential Id. + description: SNMPv2c Read Credential Id. Use Description or Id. type: str snmpV2WriteDescription: description: SNMPv2c Write Credential Description. type: str snmpV2WriteId: - description: SNMPv2c Write Credential Id. + description: SNMPv2c Write Credential Id. Use Description or Id. type: str snmpV3Description: description: SNMPv3 Credential Description. type: str snmpV3Id: - description: SNMPv3 Credential Id. + description: SNMPv3 Credential Id. Use Description or Id. type: str requirements: - dnacentersdk >= 2.5.5 @@ -266,321 +300,327 @@ """ EXAMPLES = r""" -- name: Create Credentials and assign it to a site. - 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: merged - config: - - GlobalCredentialDetails: - cliCredential: - - description: string - username: string - password: string - enablePassword: string - snmpV2cRead: - - description: string - readCommunity: string - snmpV2cWrite: - - description: string - writeCommunity: string - snmpV3: - - authPassword: string - authType: SHA - snmpMode: AUTHPRIV - privacyPassword: string - privacyType: AES128 - username: string - description: string - httpsRead: - - description: string - username: string - password: string - port: 443 - httpsWrite: - - description: string - username: string - password: string - port: 443 - AssignCredentialsToSite: - cliId: string - snmpV2ReadId: string - snmpV2WriteId: string - snmpV3Id: string - httpRead: string - httpWrite: string - siteName: - - string - -- name: Create Multiple 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: merged - config: - - GlobalCredentialDetails: - cliCredential: - - description: string - username: string - password: string - enablePassword: string - - description: string - username: string - password: string - enablePassword: string - snmpV2cRead: - - description: string - readCommunity: string - - description: string - readCommunity: string - snmpV2cWrite: - - description: string - writeCommunity: string - - description: string - writeCommunity: string - snmpV3: - - authPassword: string - authType: SHA - snmpMode: AUTHPRIV - privacyPassword: string - privacyType: AES128 - username: string - description: string - - authPassword: string - authType: SHA - snmpMode: AUTHPRIV - privacyPassword: string - privacyType: AES128 - username: string - description: string - httpsRead: - - description: string - username: string - password: string - port: 443 - - description: string - username: string - password: string - port: 443 - httpsWrite: - - description: string - username: string - password: string - port: 443 - - description: string - username: string - password: string - port: 443 - -- name: Update global device credentials using id - 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: merged - config: - - GlobalCredentialDetails: - cliCredential: - - description: string - username: string - password: string - enablePassword: string - id: string - snmpV2cRead: - - description: string - readCommunity: string - id: string - snmpV2cWrite: - - description: string - writeCommunity: string - id: string - snmpV3: - - authPassword: string - authType: SHA - snmpMode: AUTHPRIV - privacyPassword: string - privacyType: AES128 - username: string - description: string - id: string - httpsRead: - - description: string - username: string - password: string - port: 443 - id: string - httpsWrite: - - description: string - username: string - password: string - port: 443 - id: string - -- name: Update multiple global device credentials using id - 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: merged - config: - - GlobalCredentialDetails: - cliCredential: - - description: string - username: string - password: string - enablePassword: string - id: string - - description: string - username: string - password: string - enablePassword: string - id: string - snmpV2cRead: - - description: string - readCommunity: string - id: string - - description: string - readCommunity: string - id: string - snmpV2cWrite: - - description: string - writeCommunity: string - id: string - - description: string - writeCommunity: string - id: string - snmpV3: - - authPassword: string - authType: SHA - snmpMode: AUTHPRIV - privacyPassword: string - privacyType: AES128 - username: string - description: string - id: string - - authPassword: string - authType: SHA - snmpMode: AUTHPRIV - privacyPassword: string - privacyType: AES128 - username: string - description: string - id: string - httpsRead: - - description: string - username: string - password: string - port: 443 - id: string - - description: string - username: string - password: string - port: 443 - id: string - httpsWrite: - - description: string - username: string - password: string - port: 443 - id: string - - description: string - username: string - password: string - port: 443 - id: string - -- name: Update global device credential name/description using old name and description. - 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: merged - config: - - GlobalCredentialDetails: - cliCredential: - - description: string - username: string - password: string - enablePassword: string - old_description: string - old_username: string - snmpV2cRead: - - description: string - readCommunity: string - old_description: string - snmpV2cWrite: - - description: string - writeCommunity: string - old_description: string - snmpV3: - - authPassword: string - authType: string - snmpMode: string - privacyPassword: string - privacyType: string - username: string - description: string - httpsRead: - - description: string - username: string - password: string - port: string - old_description: string - old_username: string - httpsWrite: - - description: string - username: string - password: string - port: string - old_description: string - old_username: string - -- name: Assign Credentials to sites using old description and username. - 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: merged - config: - - AssignCredentialsToSite: - cliDescription: string - cliUsername: string - snmpV2ReadDescription: string - snmpV2WriteDescription: string - snmpV3Description: string - httpReadDescription: string - httpReadUsername: string - httpWriteUsername: string - httpWriteDescription: string - siteName: - - string - - string +--- + - name: Create Credentials and assign it to a site. + 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: merged + config: + - GlobalCredentialDetails: + cliCredential: + - description: string + username: string + password: string + enablePassword: string + snmpV2cRead: + - description: string + readCommunity: string + snmpV2cWrite: + - description: string + writeCommunity: string + snmpV3: + - authPassword: string + authType: SHA + snmpMode: AUTHPRIV + privacyPassword: string + privacyType: AES128 + username: string + description: string + httpsRead: + - description: string + username: string + password: string + port: 443 + httpsWrite: + - description: string + username: string + password: string + port: 443 + AssignCredentialsToSite: + cliId: string + snmpV2ReadId: string + snmpV2WriteId: string + snmpV3Id: string + httpRead: string + httpWrite: string + siteName: + - string + +--- + - name: Create Multiple 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: merged + config: + - GlobalCredentialDetails: + cliCredential: + - description: string + username: string + password: string + enablePassword: string + - description: string + username: string + password: string + enablePassword: string + snmpV2cRead: + - description: string + readCommunity: string + - description: string + readCommunity: string + snmpV2cWrite: + - description: string + writeCommunity: string + - description: string + writeCommunity: string + snmpV3: + - authPassword: string + authType: SHA + snmpMode: AUTHPRIV + privacyPassword: string + privacyType: AES128 + username: string + description: string + - authPassword: string + authType: SHA + snmpMode: AUTHPRIV + privacyPassword: string + privacyType: AES128 + username: string + description: string + httpsRead: + - description: string + username: string + password: string + port: 443 + - description: string + username: string + password: string + port: 443 + httpsWrite: + - description: string + username: string + password: string + port: 443 + - description: string + username: string + password: string + port: 443 + +--- + - name: Update global device credentials using id + 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: merged + config: + - GlobalCredentialDetails: + cliCredential: + - description: string + username: string + password: string + enablePassword: string + id: string + snmpV2cRead: + - description: string + readCommunity: string + id: string + snmpV2cWrite: + - description: string + writeCommunity: string + id: string + snmpV3: + - authPassword: string + authType: SHA + snmpMode: AUTHPRIV + privacyPassword: string + privacyType: AES128 + username: string + description: string + id: string + httpsRead: + - description: string + username: string + password: string + port: 443 + id: string + httpsWrite: + - description: string + username: string + password: string + port: 443 + id: string + +--- + - name: Update multiple global device credentials using id + 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: merged + config: + - GlobalCredentialDetails: + cliCredential: + - description: string + username: string + password: string + enablePassword: string + id: string + - description: string + username: string + password: string + enablePassword: string + id: string + snmpV2cRead: + - description: string + readCommunity: string + id: string + - description: string + readCommunity: string + id: string + snmpV2cWrite: + - description: string + writeCommunity: string + id: string + - description: string + writeCommunity: string + id: string + snmpV3: + - authPassword: string + authType: SHA + snmpMode: AUTHPRIV + privacyPassword: string + privacyType: AES128 + username: string + description: string + id: string + - authPassword: string + authType: SHA + snmpMode: AUTHPRIV + privacyPassword: string + privacyType: AES128 + username: string + description: string + id: string + httpsRead: + - description: string + username: string + password: string + port: 443 + id: string + - description: string + username: string + password: string + port: 443 + id: string + httpsWrite: + - description: string + username: string + password: string + port: 443 + id: string + - description: string + username: string + password: string + port: 443 + id: string + +--- + - name: Update global device credential name/description using old name and description. + 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: merged + config: + - GlobalCredentialDetails: + cliCredential: + - description: string + username: string + password: string + enablePassword: string + old_description: string + old_username: string + snmpV2cRead: + - description: string + readCommunity: string + old_description: string + snmpV2cWrite: + - description: string + writeCommunity: string + old_description: string + snmpV3: + - authPassword: string + authType: string + snmpMode: string + privacyPassword: string + privacyType: string + username: string + description: string + httpsRead: + - description: string + username: string + password: string + port: string + old_description: string + old_username: string + httpsWrite: + - description: string + username: string + password: string + port: string + old_description: string + old_username: string + +--- + - name: Assign Credentials to sites using old description and username. + 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: merged + config: + - AssignCredentialsToSite: + cliDescription: string + cliUsername: string + snmpV2ReadDescription: string + snmpV2WriteDescription: string + snmpV3Description: string + httpReadDescription: string + httpReadUsername: string + httpWriteUsername: string + httpWriteDescription: string + siteName: + - string + - string """ @@ -754,6 +794,41 @@ def validate_input(self): self.status = "success" return self + def validate_api_response(self, response): + """ + Get the site id from the site name. + + Parameters: + self - The current object details. + response (dict) - API response + + Returns: + self + """ + + if not response: + self.msg = "response is empty" + self.status = "exited" + return self + + if not isinstance(response, dict): + self.msg = "response is not a dictionary" + self.status = "exited" + return self + + task_id = response.get("response").get("taskId") + while True: + task_details = self.get_task_details(task_id) + self.log(str(task_details)) + if task_details.get("isError") is True: + self.msg = str(task_details.get("progress")) + self.status = "failed" + return self + elif task_details.get("isError") is False: + self.result['changed'] = True + break + return self + def get_site_id(self, site_name): """ Get the site id from the site name. @@ -793,15 +868,20 @@ def get_global_credentials_params(self): self - The current object details. Returns: - global_credentials (dict) - All global device credentials details + global_credentials (dict) - All global device credentials details. """ - global_credentials = self.dnac._exec( - family="discovery", - function='get_all_global_credentials_v2', - ) - global_credentials = global_credentials.get("response") - self.log("All Global Device Credentials Details " + str(global_credentials)) + try: + global_credentials = self.dnac._exec( + family="discovery", + function='get_all_global_credentials_v2', + ) + global_credentials = global_credentials.get("response") + self.log("All Global Device Credentials Details " + str(global_credentials)) + except Exception as exec: + self.log("Error while getting global device credentials: " + str(exec)) + return None + return global_credentials def get_cli_params(self, cliDetails): @@ -821,12 +901,11 @@ def get_cli_params(self, cliDetails): if item is None: cliCredential.append(None) else: - value = {} - value.update({ + value = { "username": item.get("username"), "description": item.get("description"), "id": item.get("id") - }) + } cliCredential.append(value) return cliCredential @@ -849,11 +928,10 @@ def get_snmpV2cRead_params(self, snmpV2cReadDetails): if item is None: snmpV2cRead.append(None) else: - value = {} - value.update({ + value = { "description": item.get("description"), "id": item.get("id") - }) + } snmpV2cRead.append(value) return snmpV2cRead @@ -876,11 +954,10 @@ def get_snmpV2cWrite_params(self, snmpV2cWriteDetails): if item is None: snmpV2cWrite.append(None) else: - value = {} - value.update({ + value = { "description": item.get("description"), "id": item.get("id") - }) + } snmpV2cWrite.append(value) return snmpV2cWrite @@ -903,13 +980,12 @@ def get_httpsRead_params(self, httpsReadDetails): if item is None: httpsRead.append(None) else: - value = {} - value.update({ + value = { "description": item.get("description"), "username": item.get("username"), "port": item.get("port"), "id": item.get("id") - }) + } httpsRead.append(value) return httpsRead @@ -932,13 +1008,12 @@ def get_httpsWrite_params(self, httpsWriteDetails): if item is None: httpsWrite.append(None) else: - value = {} - value.update({ + value = { "description": item.get("description"), "username": item.get("username"), "port": item.get("port"), "id": item.get("id") - }) + } httpsWrite.append(value) return httpsWrite @@ -959,17 +1034,14 @@ def get_snmpV3_params(self, snmpV3Details): if item is None: snmpV3.append(None) else: - value = {} - value.update({ + value = { "username": item.get("username"), "description": item.get("description"), "snmpMode": item.get("snmpMode"), "id": item.get("id"), - }) + } if value.get("snmpMode") == "AUTHNOPRIV": - value.update({ - "authType": item.get("authType") - }) + value["authType"] = item.get("authType") elif value.get("snmpMode") == "AUTHPRIV": value.update({ "authType": item.get("authType"), @@ -978,20 +1050,20 @@ def get_snmpV3_params(self, snmpV3Details): snmpV3.append(value) return snmpV3 - def get_have_device_credentials(self, CredentialDetails): + def get_cli_credentials(self, CredentialDetails, global_credentials): """ - Get the current Global Device Credentials from + Get the current CLI Credential from Cisco DNA Center based on the provided playbook details. Check this API using the check_return_status. Parameters: CredentialDetails (dict) - Playbook details containing Global Device Credentials. + global_credentials (dict) - All global device credentials details. Returns: - self - The current object with updated information. + cliDetails (List) - The current CLI credentials. """ - global_credentials = self.get_global_credentials_params() # playbook CLI Credential details all_CLI = CredentialDetails.get("cliCredential") # All CLI details from Cisco DNA Center @@ -1039,6 +1111,21 @@ def get_have_device_credentials(self, CredentialDetails): return self cliDetail = item cliDetails.append(cliDetail) + return cliDetails + + def get_snmpV2cRead_credentials(self, CredentialDetails, global_credentials): + """ + Get the current snmpV2cRead Credential from + Cisco DNA Center based on the provided playbook details. + Check this API using the check_return_status. + + Parameters: + CredentialDetails (dict) - Playbook details containing Global Device Credentials. + global_credentials (dict) - All global device credentials details. + + Returns: + snmpV2cReadDetails (List) - The current snmpV2cRead. + """ # Playbook snmpV2cRead Credential details all_snmpV2cRead = CredentialDetails.get("snmpV2cRead") @@ -1077,6 +1164,21 @@ def get_have_device_credentials(self, CredentialDetails): snmpV2cReadDescription ) snmpV2cReadDetails.append(snmpV2cReadDetail) + return snmpV2cReadDetails + + def get_snmpV2cWrite_credentials(self, CredentialDetails, global_credentials): + """ + Get the current snmpV2cWrite Credential from + Cisco DNA Center based on the provided playbook details. + Check this API using the check_return_status. + + Parameters: + CredentialDetails (dict) - Playbook details containing Global Device Credentials. + global_credentials (dict) - All global device credentials details. + + Returns: + snmpV2cWriteDetails (List) - The current snmpV2cWrite. + """ # Playbook snmpV2cWrite Credential details all_snmpV2cWrite = CredentialDetails.get("snmpV2cWrite") @@ -1115,6 +1217,21 @@ def get_have_device_credentials(self, CredentialDetails): snmpV2cWriteDescription ) snmpV2cWriteDetails.append(snmpV2cWriteDetail) + return snmpV2cWriteDetails + + def get_httpsRead_credentials(self, CredentialDetails, global_credentials): + """ + Get the current httpsRead Credential from + Cisco DNA Center based on the provided playbook details. + Check this API using the check_return_status. + + Parameters: + CredentialDetails (dict) - Playbook details containing Global Device Credentials. + global_credentials (dict) - All global device credentials details. + + Returns: + httpsReadDetails (List) - The current httpsRead. + """ # Playbook httpsRead Credential details all_httpsRead = CredentialDetails.get("httpsRead") @@ -1163,6 +1280,21 @@ def get_have_device_credentials(self, CredentialDetails): return self httpsReadDetail = item httpsReadDetails.append(httpsReadDetail) + return httpsReadDetails + + def get_httpsWrite_credentials(self, CredentialDetails, global_credentials): + """ + Get the current httpsWrite Credential from + Cisco DNA Center based on the provided playbook details. + Check this API using the check_return_status. + + Parameters: + CredentialDetails (dict) - Playbook details containing Global Device Credentials. + global_credentials (dict) - All global device credentials details. + + Returns: + httpsWriteDetails (List) - The current httpsWrite. + """ # Playbook httpsWrite Credential details all_httpsWrite = CredentialDetails.get("httpsWrite") @@ -1207,6 +1339,21 @@ def get_have_device_credentials(self, CredentialDetails): and item.get("username") == httpsWriteUsername: httpsWriteDetail = item httpsWriteDetails.append(httpsWriteDetail) + return httpsWriteDetails + + def get_snmpV3_credentials(self, CredentialDetails, global_credentials): + """ + Get the current snmpV3 Credential from + Cisco DNA Center based on the provided playbook details. + Check this API using the check_return_status. + + Parameters: + CredentialDetails (dict) - Playbook details containing Global Device Credentials. + global_credentials (dict) - All global device credentials details. + + Returns: + snmpV3Details (List) - The current snmpV3. + """ # Playbook snmpV3 Credential details all_snmpV3 = CredentialDetails.get("snmpV3") @@ -1238,7 +1385,29 @@ def get_have_device_credentials(self, CredentialDetails): if snmpV3Description and (not snmpV3Detail): snmpV3Detail = get_dict_result(snmpV3_details, "description", snmpV3Description) snmpV3Details.append(snmpV3Detail) + return snmpV3Details + + def get_have_device_credentials(self, CredentialDetails): + """ + Get the current Global Device Credentials from + Cisco DNA Center based on the provided playbook details. + Check this API using the check_return_status. + + Parameters: + CredentialDetails (dict) - Playbook details containing Global Device Credentials. + + Returns: + self - The current object with updated information. + """ + global_credentials = self.get_global_credentials_params() + cliDetails = self.get_cli_credentials(CredentialDetails, global_credentials) + snmpV2cReadDetails = self.get_snmpV2cRead_credentials(CredentialDetails, global_credentials) + snmpV2cWriteDetails = self.get_snmpV2cWrite_credentials(CredentialDetails, + global_credentials) + httpsReadDetails = self.get_httpsRead_credentials(CredentialDetails, global_credentials) + httpsWriteDetails = self.get_httpsWrite_credentials(CredentialDetails, global_credentials) + snmpV3Details = self.get_snmpV3_credentials(CredentialDetails, global_credentials) self.have.update({"globalCredential": {}}) if cliDetails: cliCredential = self.get_cli_params(cliDetails) @@ -1952,7 +2121,7 @@ def create_device_credentials(self): Check the return value of the API with check_return_status(). Parameters: - None + self Returns: self @@ -1977,27 +2146,7 @@ def create_device_credentials(self): params=credential_params, ) self.log(str(response)) - if not response: - self.msg = "response is empty" - self.status = "exited" - return self - - if not isinstance(response, dict): - self.msg = "response is not a dictionary" - self.status = "exited" - return self - - task_id = response.get("response").get("taskId") - while True: - task_details = self.get_task_details(task_id) - self.log(str(task_details)) - if task_details.get("isError") is True: - self.msg = str(task_details.get("progress")) - self.status = "failed" - return self - elif task_details.get("isError") is False: - self.result['changed'] = True - break + self.validate_api_response(response).check_return_status() self.log("Global Credential Created Successfully") result_global_credential.update({ "Creation": { @@ -2015,14 +2164,17 @@ def update_device_credentials(self): Check the return value of the API with check_return_status(). Parameters: - None + self Returns: self """ result_global_credential = self.result.get("response")[0].get("globalCredential") + + # Get the result global credential and want_update from the current object want_update = self.want.get("want_update") + # If no credentials to update, update the result and return if not want_update: result_global_credential.update({ "No Updation": { @@ -2055,27 +2207,7 @@ def update_device_credentials(self): params=credential_params, ) self.log(str(response)) - if not response: - self.msg = "response is empty" - self.status = "exited" - return self - - if not isinstance(response, dict): - self.msg = "response is not a dictionary" - self.status = "exited" - return self - - task_id = response.get("response").get("taskId") - while True: - task_details = self.get_task_details(task_id) - self.log(str(task_details)) - if task_details.get("isError") is True: - self.msg = str(task_details.get("progress")) - self.status = "failed" - return self - elif task_details.get("isError") is False: - self.result['changed'] = True - break + self.validate_api_response(response).check_return_status() self.log("Update Device Credential API input - " + str(final_response)) self.log("Global Device Credential Updated Successfully") result_global_credential.update({ @@ -2095,7 +2227,7 @@ def assign_credentials_to_site(self): Check the return value of the API with check_return_status(). Parameters: - None + self Returns: self @@ -2115,7 +2247,9 @@ def assign_credentials_to_site(self): self.msg = "No Assignment is available" self.status = "success" return self - for site_id in self.want.get("site_id"): + + 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) response = self.dnac._exec( @@ -2124,28 +2258,7 @@ def assign_credentials_to_site(self): params=credential_params, ) self.log(str(response)) - if not response: - self.msg = "response is empty" - self.status = "failed" - return self - - if not isinstance(response, dict): - self.msg = "response is not a dictionary" - self.status = "failed" - return self - - task_id = response.get("response").get("taskId") - while True: - task_details = self.get_task_details(task_id) - self.log(str(task_details)) - if task_details.get("isError") is True: - self.msg = str(task_details.get("progress")) - self.status = "failed" - return self - elif task_details.get("isError") is False: - self.result['changed'] = True - break - + self.validate_api_response(response).check_return_status() self.log("Device Credential Assigned to site is Successfully") result_assign_credential.update({ "Assign Credentials": { @@ -2187,10 +2300,11 @@ def delete_device_credential(self, config): Check the return value of the API with check_return_status(). Parameters: - None + config (dict) - Playbook details containing Global Device Credential information. + self - The current object details. Returns: - None + self """ result_global_credential = self.result.get("response")[0].get("globalCredential") @@ -2213,26 +2327,7 @@ def delete_device_credential(self, config): params={"id": _id}, ) self.log(str(response)) - if not response: - self.msg = "response is empty" - self.status = "exited" - return self - - if not isinstance(response, dict): - self.msg = "response is not a dictionary" - self.status = "exited" - return self - - task_id = response.get("response").get("taskId") - while True: - task_details = self.get_task_details(task_id) - if task_details.get("isError") is True: - self.msg = str(task_details.get("progress")) - self.status = "failed" - return self - elif task_details.get("isError") is False: - self.result['changed'] = True - break + self.validate_api_response(response).check_return_status() final_response.get(item).append(_id) config_itr = config_itr + 1 @@ -2253,10 +2348,11 @@ def get_diff_deleted(self, config): Delete Global Device Credential in Cisco DNA Center based on the playbook details. Parameters: - None + config (dict) - Playbook details containing Global Device Credential information. + self - The current object details. Returns: - None + self """ if config.get("GlobalCredentialDetails") is not None: @@ -2269,15 +2365,15 @@ def reset_values(self): Reset all neccessary attributes to default values Parameters: - None + self Returns: - None + self """ self.have.clear() self.want.clear() - return + return self def main(): From fdd243ef5be8b4f13031442c375458f00eb6c258 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 3 Nov 2023 15:41:59 +0530 Subject: [PATCH 20/32] Addressed PR comment --- plugins/modules/device_credential_intent.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index 0f6783ba7d..a42dc92601 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -352,7 +352,6 @@ siteName: - string ---- - name: Create Multiple Credentials. cisco.dnac.device_credential_intent: dnac_host: "{{ dnac_host }}" @@ -418,7 +417,6 @@ password: string port: 443 ---- - name: Update global device credentials using id cisco.dnac.device_credential_intent: dnac_host: "{{ dnac_host }}" @@ -467,7 +465,6 @@ port: 443 id: string ---- - name: Update multiple global device credentials using id cisco.dnac.device_credential_intent: dnac_host: "{{ dnac_host }}" @@ -545,7 +542,6 @@ port: 443 id: string ---- - name: Update global device credential name/description using old name and description. cisco.dnac.device_credential_intent: dnac_host: "{{ dnac_host }}" @@ -596,7 +592,6 @@ old_description: string old_username: string ---- - name: Assign Credentials to sites using old description and username. cisco.dnac.device_credential_intent: dnac_host: "{{ dnac_host }}" From c4e40d95def86ada0075bd6ec9e02490b67f7200 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Fri, 3 Nov 2023 16:02:41 +0530 Subject: [PATCH 21/32] Addressed the PR Comments --- plugins/module_utils/dnac.py | 35 +++++++++++++++++ plugins/modules/device_credential_intent.py | 43 ++------------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 55ee393a97..6acfdba568 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -158,6 +158,41 @@ def get_task_details(self, task_id): return result + def check_task_response_status(self, response): + """ + Get the site id from the site name. + + Parameters: + self - The current object details. + response (dict) - API response + + Returns: + self + """ + + if not response: + self.msg = "response is empty" + self.status = "exited" + return self + + if not isinstance(response, dict): + self.msg = "response is not a dictionary" + self.status = "exited" + return self + + task_id = response.get("response").get("taskId") + while True: + task_details = self.get_task_details(task_id) + self.log(str(task_details)) + if task_details.get("isError") is True: + self.msg = str(task_details.get("progress")) + self.status = "failed" + return self + elif task_details.get("isError") is False: + self.result['changed'] = True + break + return self + def reset_values(self): """Reset all neccessary attributes to default values""" diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index a42dc92601..a7b95a0fe4 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -789,41 +789,6 @@ def validate_input(self): self.status = "success" return self - def validate_api_response(self, response): - """ - Get the site id from the site name. - - Parameters: - self - The current object details. - response (dict) - API response - - Returns: - self - """ - - if not response: - self.msg = "response is empty" - self.status = "exited" - return self - - if not isinstance(response, dict): - self.msg = "response is not a dictionary" - self.status = "exited" - return self - - task_id = response.get("response").get("taskId") - while True: - task_details = self.get_task_details(task_id) - self.log(str(task_details)) - if task_details.get("isError") is True: - self.msg = str(task_details.get("progress")) - self.status = "failed" - return self - elif task_details.get("isError") is False: - self.result['changed'] = True - break - return self - def get_site_id(self, site_name): """ Get the site id from the site name. @@ -2141,7 +2106,7 @@ def create_device_credentials(self): params=credential_params, ) self.log(str(response)) - self.validate_api_response(response).check_return_status() + self.check_task_response_status(response).check_return_status() self.log("Global Credential Created Successfully") result_global_credential.update({ "Creation": { @@ -2202,7 +2167,7 @@ def update_device_credentials(self): params=credential_params, ) self.log(str(response)) - self.validate_api_response(response).check_return_status() + self.check_task_response_status(response).check_return_status() self.log("Update Device Credential API input - " + str(final_response)) self.log("Global Device Credential Updated Successfully") result_global_credential.update({ @@ -2253,7 +2218,7 @@ def assign_credentials_to_site(self): params=credential_params, ) self.log(str(response)) - self.validate_api_response(response).check_return_status() + self.check_task_response_status(response).check_return_status() self.log("Device Credential Assigned to site is Successfully") result_assign_credential.update({ "Assign Credentials": { @@ -2322,7 +2287,7 @@ def delete_device_credential(self, config): params={"id": _id}, ) self.log(str(response)) - self.validate_api_response(response).check_return_status() + self.check_task_response_status(response).check_return_status() final_response.get(item).append(_id) config_itr = config_itr + 1 From fa2ffd32570bf693110fc19265c9cd9ec60e4270 Mon Sep 17 00:00:00 2001 From: MUTHU-RAKESH-27 <19cs127@psgitech.ac.in> Date: Sat, 4 Nov 2023 16:13:23 +0530 Subject: [PATCH 22/32] Addressed the PR comments --- plugins/module_utils/dnac.py | 16 ++++++++++++---- plugins/modules/device_credential_intent.py | 12 ++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 6acfdba568..98290e5dd1 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -158,13 +158,14 @@ def get_task_details(self, task_id): return result - def check_task_response_status(self, response): + def check_task_response_status(self, response, validation_string): """ Get the site id from the site name. Parameters: self - The current object details. - response (dict) - API response + response (dict) - API response. + validation_string (string) - String used to match the progress status. Returns: self @@ -184,13 +185,20 @@ def check_task_response_status(self, response): while True: task_details = self.get_task_details(task_id) self.log(str(task_details)) + if task_details.get("isError") is True: self.msg = str(task_details.get("progress")) self.status = "failed" - return self - elif task_details.get("isError") is False: + break + + if validation_string in task_details.get("progress").lower(): self.result['changed'] = True + self.status = "success" break + + self.log("progress set to {0} for taskid: {1}" + .format(task_details.get('progress'), task_id)) + return self def reset_values(self): diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index a7b95a0fe4..320121ec52 100644 --- a/plugins/modules/device_credential_intent.py +++ b/plugins/modules/device_credential_intent.py @@ -2106,7 +2106,8 @@ def create_device_credentials(self): params=credential_params, ) self.log(str(response)) - self.check_task_response_status(response).check_return_status() + validation_string = "global credential addition performed" + self.check_task_response_status(response, validation_string).check_return_status() self.log("Global Credential Created Successfully") result_global_credential.update({ "Creation": { @@ -2167,7 +2168,8 @@ def update_device_credentials(self): params=credential_params, ) self.log(str(response)) - self.check_task_response_status(response).check_return_status() + validation_string = "global credential update performed" + self.check_task_response_status(response, validation_string).check_return_status() self.log("Update Device Credential API input - " + str(final_response)) self.log("Global Device Credential Updated Successfully") result_global_credential.update({ @@ -2218,7 +2220,8 @@ def assign_credentials_to_site(self): params=credential_params, ) self.log(str(response)) - self.check_task_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("Device Credential Assigned to site is Successfully") result_assign_credential.update({ "Assign Credentials": { @@ -2287,7 +2290,8 @@ def delete_device_credential(self, config): params={"id": _id}, ) self.log(str(response)) - self.check_task_response_status(response).check_return_status() + validation_string = "global credential deleted successfully" + self.check_task_response_status(response, validation_string).check_return_status() final_response.get(item).append(_id) config_itr = config_itr + 1 From 9d91884dc9aa8d138f6ebe7f1ab6776984c4ba2c Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Tue, 7 Nov 2023 10:41:52 +0000 Subject: [PATCH 23/32] Resolved the previous PR comments and have added single addition in PNP module --- playbooks/PnP.yml | 82 +- plugins/modules/pnp_intent.py | 717 +++++------------- tests/unit/modules/dnac/dnac_module.py | 108 ++- tests/unit/modules/dnac/test_pnp_intent.py | 87 +++ tests/unit/modules/dnac/test_site_intent.py | 71 ++ tests/unit/modules/dnac/test_swim_intent.py | 107 +++ .../unit/modules/dnac/test_template_intent.py | 72 ++ 7 files changed, 684 insertions(+), 560 deletions(-) diff --git a/playbooks/PnP.yml b/playbooks/PnP.yml index 954c2bbb12..be53564c03 100644 --- a/playbooks/PnP.yml +++ b/playbooks/PnP.yml @@ -1,5 +1,5 @@ --- -- name: Manage operations add device, claim device and unclaim device of Onboarding Configuration(PnP) +- name: Manage operations - Add, claim, and delete devices of Onboarding Configuration (PnP) hosts: localhost connection: local gather_facts: no @@ -19,35 +19,77 @@ tasks: - - name: Add a new device and claim the device + - name: Add a new device only cisco.dnac.pnp_intent: <<: *dnac_login dnac_log: True - state: deleted + state: merged config: - - template_name: "Ansible_PNP_WLC" - image_name: C9800-40-universalk9_wlc.17.12.01.SPA.bin - golden_image: True - site_name: Global/USA/San Francisco/BGL_18 - pnp_type: CatalystWLC + - deviceInfo: + add_device_method: Single + serialNumber: FJC2330E0IK + hostname: Test-9300-6 + state: Unclaimed + pid: c9300-24P + isSudiRequired: True + + - name: Add a new device and claim it + cisco.dnac.pnp_intent: + <<: *dnac_login + dnac_log: True + state: merged + config: + - site_name: Global/USA/San Francisco/BGL_18 deviceInfo: - serialNumber: FOX2639PAY7 - hostname: WLC + add_device_method: Single + serialNumber: FJC2330E0IK + hostname: Test-9300-6 state: Unclaimed - pid: C9800-40-K9 - projectName: Onboarding Configuration - staticIP: 204.192.101.10 - subnetMask: 255.255.255.0 - gateway: 204.192.101.1 - vlanId: 1101 - ipInterfaceName: TenGigabitEthernet0/0/0 + pid: c9300-24P + isSudiRequired: True - - template_name: "Ansible_PNP_Switch" + - name: Claim an added Switch with template and image upgrade to a site only + cisco.dnac.pnp_intent: + <<: *dnac_login + dnac_log: True + state: merged + config: + - site_name: Global/USA/San Francisco/BGL_18 + template_name: "Ansible_PNP_Switch" image_name: cat9k_iosxe_npe.17.03.07.SPA.bin - site_name: Global/USA/San Francisco/BGL_18 + project_name: Onboarding Configuration deviceInfo: serialNumber: FJC271924EQ hostname: Switch state: Unclaimed pid: C9300-48UXM - projectName: Onboarding Configuration + + - name: Claim an added Wireless Controller with template and image upgrade to a site only + cisco.dnac.pnp_intent: + <<: *dnac_login + dnac_log: True + state: merged + config: + - site_name: Global/USA/San Francisco/BGL_18 + pnp_type: CatalystWLC + 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 + gateway: 204.192.101.1 + ipInterfaceName: TenGigabitEthernet0/0/0 + staticIP: 204.192.101.10 + subnetMask: 255.255.255.0 + vlanId: 1101 + + - name: Delete an added device from the Pnp dashboard + cisco.dnac.pnp_intent: + <<: *dnac_login + dnac_log: True + state: deleted + config: + - deviceInfo: + serialNumber: FJC2330E0IK \ No newline at end of file diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 5a289ad27c..bb028cbade 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -47,388 +47,79 @@ site_name: description: Name of the site for which device will be claimed. type: str + projectName: + 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) + type: str + default: Default + staticIP: + description: Management IP address of the Wireless Controller + type: str + subnetMask: + 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: + description: Vlan Id allocated for claimimg of Wireless Controller + type: str + ipInterfaceName: + 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) + type: str deviceInfo: description: Pnp Device's deviceInfo. type: dict + required: true suboptions: - aaaCredentials: - description: Pnp Device's aaaCredentials. - type: dict - suboptions: - password: - description: Pnp Device's password. - type: str - username: - description: Pnp Device's username. - type: str - addedOn: - description: Pnp Device's addedOn. - type: int - addnMacAddrs: - description: Pnp Device's addnMacAddrs. - elements: str - type: list - agentType: - description: Pnp Device's agentType. - type: str - authStatus: - description: Pnp Device's authStatus. - type: str - authenticatedSudiSerialNo: - description: Pnp Device's authenticatedSudiSerialNo. - type: str - capabilitiesSupported: - description: Pnp Device's capabilitiesSupported. - elements: str - type: list - cmState: - description: Pnp Device's cmState. - type: str - description: - description: Pnp Device's description. - type: str - deviceSudiSerialNos: - description: Pnp Device's deviceSudiSerialNos. - elements: str - type: list - deviceType: - description: Pnp Device's deviceType. - type: str - featuresSupported: - description: Pnp Device's featuresSupported. - elements: str - type: list - fileSystemList: - description: Pnp Device's fileSystemList. - type: list - elements: dict - suboptions: - freespace: - description: Pnp Device's freespace. - type: int - name: - description: Pnp Device's name. - type: str - readable: - description: Readable flag. - type: bool - size: - description: Pnp Device's size. - type: int - type: - description: Pnp Device's type. - type: str - writeable: - description: Writeable flag. - type: bool - firstContact: - description: Pnp Device's firstContact. - type: int hostname: description: Pnp Device's hostname. type: str - httpHeaders: - description: Pnp Device's httpHeaders. - type: list - elements: dict - suboptions: - key: - description: Pnp Device's key. - type: str - value: - description: Pnp Device's value. - type: str - imageFile: - description: Pnp Device's imageFile. - type: str - imageVersion: - description: Pnp Device's imageVersion. - type: str - ipInterfaces: - description: Pnp Device's ipInterfaces. - elements: dict - type: list - suboptions: - ipv4Address: - description: Pnp Device's ipv4Address. - type: dict - ipv6AddressList: - description: Pnp Device's ipv6AddressList. - elements: dict - type: list - macAddress: - description: Pnp Device's macAddress. - type: str - name: - description: Pnp Device's name. - type: str - status: - description: Pnp Device's status. - type: str - lastContact: - description: Pnp Device's lastContact. - type: int - lastSyncTime: - description: Pnp Device's lastSyncTime. - type: int - lastUpdateOn: - description: Pnp Device's lastUpdateOn. - type: int - location: - description: Pnp Device's location. - type: dict - suboptions: - address: - description: Pnp Device's address. - type: str - altitude: - description: Pnp Device's altitude. - type: str - latitude: - description: Pnp Device's latitude. - type: str - longitude: - description: Pnp Device's longitude. - type: str - siteId: - description: Pnp Device's siteId. - type: str - macAddress: - description: Pnp Device's macAddress. - type: str - mode: - description: Pnp Device's mode. - type: str - name: - description: Pnp Device's name. - type: str - neighborLinks: - description: Pnp Device's neighborLinks. - type: list - elements: dict - suboptions: - localInterfaceName: - description: Pnp Device's localInterfaceName. - type: str - localMacAddress: - description: Pnp Device's localMacAddress. - type: str - localShortInterfaceName: - description: Pnp Device's localShortInterfaceName. - type: str - remoteDeviceName: - description: Pnp Device's remoteDeviceName. - type: str - remoteInterfaceName: - description: Pnp Device's remoteInterfaceName. - type: str - remoteMacAddress: - description: Pnp Device's remoteMacAddress. - type: str - remotePlatform: - description: Pnp Device's remotePlatform. - type: str - remoteShortInterfaceName: - description: Pnp Device's remoteShortInterfaceName. - type: str - remoteVersion: - description: Pnp Device's remoteVersion. - type: str - onbState: - description: Pnp Device's onbState. + state: + description: Pnp Device's onbording state (Unclaimed/Claimed/Provisioned). type: str pid: description: Pnp Device's pid. type: str - pnpProfileList: - description: Pnp Device's pnpProfileList. - type: list - elements: dict - suboptions: - createdBy: - description: Pnp Device's createdBy. - type: str - discoveryCreated: - description: DiscoveryCreated flag. - type: bool - primaryEndpoint: - description: Pnp Device's primaryEndpoint. - type: dict - suboptions: - certificate: - description: Pnp Device's certificate. - type: str - fqdn: - description: Pnp Device's fqdn. - type: str - ipv4Address: - description: Pnp Device's ipv4Address. - type: dict - ipv6Address: - description: Pnp Device's ipv6Address. - type: dict - port: - description: Pnp Device's port. - type: int - protocol: - description: Pnp Device's protocol. - type: str - profileName: - description: Pnp Device's profileName. - type: str - secondaryEndpoint: - description: Pnp Device's secondaryEndpoint. - type: dict - suboptions: - certificate: - description: Pnp Device's certificate. - type: str - fqdn: - description: Pnp Device's fqdn. - type: str - ipv4Address: - description: Pnp Device's ipv4Address. - type: dict - ipv6Address: - description: Pnp Device's ipv6Address. - type: dict - port: - description: Pnp Device's port. - type: int - protocol: - description: Pnp Device's protocol. - type: str - populateInventory: - description: PopulateInventory flag. - type: bool - preWorkflowCliOuputs: - description: Pnp Device's preWorkflowCliOuputs. - type: list - elements: dict - suboptions: - cli: - description: Pnp Device's cli. - type: str - cliOutput: - description: Pnp Device's cliOutput. - type: str - projectId: - description: Pnp Device's projectId. - type: str - projectName: - description: Pnp Device's projectName. - type: str - reloadRequested: - description: ReloadRequested flag. - type: bool serialNumber: description: Pnp Device's serialNumber. type: str - smartAccountId: - description: Pnp Device's smartAccountId. + add_device_method: + description: Pnp Device's device addition method (Single/Bulk/Smart Account). type: str - source: - description: Pnp Device's source. - type: str - stack: - description: Stack flag. - type: bool - stackInfo: - description: Pnp Device's stackInfo. - type: dict - suboptions: - isFullRing: - description: IsFullRing flag. - type: bool - stackMemberList: - description: Pnp Device's stackMemberList. - type: list - elements: dict - suboptions: - hardwareVersion: - description: Pnp Device's hardwareVersion. - type: str - licenseLevel: - description: Pnp Device's licenseLevel. - type: str - licenseType: - description: Pnp Device's licenseType. - type: str - macAddress: - description: Pnp Device's macAddress. - type: str - pid: - description: Pnp Device's pid. - type: str - priority: - description: Pnp Device's priority. - type: int - role: - description: Pnp Device's role. - type: str - serialNumber: - description: Pnp Device's serialNumber. - type: str - softwareVersion: - description: Pnp Device's softwareVersion. - type: str - stackNumber: - description: Pnp Device's stackNumber. - type: int - state: - description: Pnp Device's state. - type: str - sudiSerialNumber: - description: Pnp Device's sudiSerialNumber. - type: str - stackRingProtocol: - description: Pnp Device's stackRingProtocol. - type: str - supportsStackWorkflows: - description: SupportsStackWorkflows flag. - type: bool - totalMemberCount: - description: Pnp Device's totalMemberCount. - type: int - validLicenseLevels: - description: Pnp Device's validLicenseLevels. - type: str - state: - description: Pnp Device's state. - type: str - sudiRequired: - description: SudiRequired flag. + isSudiRequired: + description: Sudi Authentication requiremnet's flag. type: bool - tags: - description: Pnp Device's tags. - type: dict - userSudiSerialNos: - description: Pnp Device's userSudiSerialNos. - elements: str - type: list - virtualAccountId: - description: Pnp Device's virtualAccountId. - type: str - workflowId: - description: Pnp Device's workflowId. - type: str - workflowName: - description: Pnp Device's workflowName. - type: str requirements: -- dnacentersdk == 2.4.5 +- dnacentersdk == 2.6.5 - python >= 3.5 notes: - SDK Method used are device_onboarding_pnp.DeviceOnboardingPnp.add_device, + device_onboarding_pnp.DeviceOnboardingPnp.get_device_list, 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, + sites.Sites.get_site, + software_image_management_swim.SoftwareImageManagementSwim.get_software_image_details, + configuration_templates.ConfigurationTemplates.gets_the_templates_available - Paths used are post /dna/intent/api/v1/onboarding/pnp-device post /dna/intent/api/v1/onboarding/pnp-device/site-claim post /dna/intent/api/v1/onboarding/pnp-device/{id} + get /dna/intent/api/v1/onboarding/pnp-device/count + get /dna/intent/api/v1/onboarding/pnp-device + get /dna/intent/api/v1/site + get /dna/intent/api/v1/image/importation + get /dna/intent/api/v1/template-programmer/template """ @@ -445,129 +136,25 @@ dnac_log: True state: merged config: - template_name: string - image_name: string - site_name: string - deviceInfo: - aaaCredentials: - password: string - username: string - addedOn: 0 - addnMacAddrs: - - string - agentType: string - authStatus: string - authenticatedSudiSerialNo: string - capabilitiesSupported: - - string - cmState: string - description: string - deviceSudiSerialNos: - - string - deviceType: string - featuresSupported: - - string - fileSystemList: - - freespace: 0 - name: string - readable: true - size: 0 - type: string - writeable: true - firstContact: 0 - hostname: string - httpHeaders: - - key: string - value: string - imageFile: string - imageVersion: string - ipInterfaces: - - ipv4Address: {} - ipv6AddressList: - - {} - macAddress: string - name: string - status: string - lastContact: 0 - lastSyncTime: 0 - lastUpdateOn: 0 - location: - address: string - altitude: string - latitude: string - longitude: string - siteId: string - macAddress: string - mode: string - name: string - neighborLinks: - - localInterfaceName: string - localMacAddress: string - localShortInterfaceName: string - remoteDeviceName: string - remoteInterfaceName: string - remoteMacAddress: string - remotePlatform: string - remoteShortInterfaceName: string - remoteVersion: string - onbState: string - pid: string - pnpProfileList: - - createdBy: string - discoveryCreated: true - primaryEndpoint: - certificate: string - fqdn: string - ipv4Address: {} - ipv6Address: {} - port: 0 - protocol: string - profileName: string - secondaryEndpoint: - certificate: string - fqdn: string - ipv4Address: {} - ipv6Address: {} - port: 0 - protocol: string - populateInventory: true - preWorkflowCliOuputs: - - cli: string - cliOutput: string - projectId: string - projectName: string - reloadRequested: true - serialNumber: string - smartAccountId: string - source: string - stack: true - stackInfo: - isFullRing: true - stackMemberList: - - hardwareVersion: string - licenseLevel: string - licenseType: string - macAddress: string + - template_name: string + image_name: string + golden_image: bool + site_name: string + projectName: string + pnp_type: string + staticIP: string + subnetMask: string + gateway: string + vlanId: string + ipInterfaceName: string + rfProfile: string + deviceInfo: + hostname: string + state: string pid: string - priority: 0 - role: string serialNumber: string - softwareVersion: string - stackNumber: 0 - state: string - sudiSerialNumber: string - stackRingProtocol: string - supportsStackWorkflows: true - totalMemberCount: 0 - validLicenseLevels: string - state: string - sudiRequired: true - tags: {} - userSudiSerialNos: - - string - virtualAccountId: string - workflowId: string - workflowName: string + add_device_method: string + isSudiRequired: string """ RETURN = r""" @@ -608,7 +195,6 @@ "msg": String } """ - from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( DnacBase, @@ -682,7 +268,6 @@ def validate_input(self): self.msg = "Successfully validated input" self.status = "success" - return self def site_exists(self): @@ -716,8 +301,9 @@ def site_exists(self): if response: self.log(str(response)) site = response.get("response") - site_id = site[0].get("id") - site_exists = True + if len(site) == 1: + site_id = site[0].get("id") + site_exists = True return (site_exists, site_id) @@ -771,15 +357,7 @@ def get_pnp_params(self, params): """ pnp_params = { - '_id': params.get('_id'), - 'deviceInfo': params.get('deviceInfo'), - 'runSummaryList': params.get('runSummaryList'), - 'systemResetWorkflow': params.get('systemResetWorkflow'), - 'systemWorkflow': params.get('systemWorkflow'), - 'tenantId': params.get('tenantId'), - 'version': params.get('device_version'), - 'workflow': params.get('workflow'), - 'workflowParameters': params.get('workflowParameters') + 'deviceInfo': params.get('deviceInfo') } return pnp_params @@ -892,9 +470,12 @@ def get_have(self): # check if given site exits, if exists store current site info site_exists = False - if isinstance(self.want.get("site_name"), str): - site_name = self.want.get("site_name") - (site_exists, site_id) = self.site_exists() + if not isinstance(self.want.get("site_name"), str) and \ + not self.want.get('pnp_params').get('deviceInfo').get('add_device_method'): + self.module.fail_json(msg="Name of the site must be a string", response=[]) + + site_name = self.want.get("site_name") + (site_exists, site_id) = self.site_exists() if site_exists: have["site_id"] = site_id @@ -904,22 +485,29 @@ def get_have(self): if self.get_site_type() != "floor": self.module.fail_json(msg="Type of the site must \ be a floor for claiming an AP", response=[]) + if len(image_list) == 1: have["image_id"] = image_list[0].get("imageUuid") self.log("Image Id: " + str(have["image_id"])) - if template_list and isinstance(template_list, list): - # API execution error returns a dict - if self.want.get("template_name"): - template_details = get_dict_result(template_list, 'name', self.want.get("template_name")) + + template_name = self.want.get("template_name") + if template_name: + if template_list and isinstance(template_list, list): + + template_details = get_dict_result(template_list, 'name', template_name) if template_details: have["template_id"] = template_details.get("templateId") else: self.module.fail_json(msg="Template Not Found", response=[]) - else: - self.module.fail_json(msg="Project Not Found or Project is Empty", response=[]) + + else: + self.module.fail_json(msg="Project Not Found \ + or Project is Empty", response=[]) + else: - self.module.fail_json(msg="Site not found", response=[]) + if not self.want.get('pnp_params').get('deviceInfo').get('add_device_method'): + self.module.fail_json(msg="Site not found", response=[]) # check if given device exists in pnp inventory, store device Id device_response = self.dnac_apply['exec']( @@ -971,19 +559,23 @@ def get_want(self, config): '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') + 'template_name': config.get('template_name'), + 'add_device_method': config.get('deviceInfo').get('add_device_method'), + 'isSudiRequired': config.get('deviceInfo').get('isSudiRequired') } - if self.want["pnp_type"] == "CatalystWLC": - self.want["staticIP"] = config.get('staticIP') - self.want["subnetMask"] = config.get('subnetMask') - self.want["gateway"] = config.get('gateway') - self.want["vlanId"] = config.get('vlanId') - self.want["ipInterfaceName"] = config.get('ipInterfaceName') + if self.want["pnp_type"] != "Default": + + if self.want["pnp_type"] == "CatalystWLC": + self.want["staticIP"] = config.get('staticIP') + self.want["subnetMask"] = config.get('subnetMask') + self.want["gateway"] = config.get('gateway') + self.want["vlanId"] = config.get('vlanId') + self.want["ipInterfaceName"] = config.get('ipInterfaceName') - if self.want["pnp_type"] == "AccessPoint": - if self.get_site_type == "floor": - self.want["rfProfile"] = config.get("rfProfile") + elif self.want["pnp_type"] == "AccessPoint": + if self.get_site_type() == "floor": + self.want["rfProfile"] = config.get("rfProfile") self.msg = "Successfully collected all parameters from playbook " + \ "for comparison" @@ -1008,36 +600,95 @@ def get_diff_merged(self): class instance for further use. """ - if not self.have.get("device_found"): - self.log("Adding device to pnp database") - response = self.dnac_apply['exec']( - family="device_onboarding_pnp", - function="add_device", - params=self.want.get("pnp_params"), - op_modifies=True, - ) - - self.have["device_id"] = response.get("id") - self.log(str(response)) - self.log(self.have.get("device_id")) + device_count_params = { + "serial_number": self.want.get("serial_number"), + "state": "Provisioned" + } - claim_params = self.get_claim_params() - claim_response = self.dnac_apply['exec']( - family="device_onboarding_pnp", - function='claim_a_device_to_a_site', - op_modifies=True, - params=claim_params, - ) + if not self.have.get("device_found"): + if not self.want["add_device_method"]: + self.module.fail_json(msg="Device needs to be added before claiming", response=[]) + else: + 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.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.module.fail_json(msg="Device Addition Failed", response=[]) - self.log(str(claim_response)) + 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)) + claim_params = self.get_claim_params() + claim_params["deviceId"] = dev_add_response.get("id") + claim_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='claim_a_device_to_a_site', + op_modifies=True, + params=claim_params, + ) + + self.log(str(claim_response)) + if claim_response.get("response") == "Device Claimed" \ + and self.have["deviceInfo"]: + self.result['msg'] = "Device Added and Claimed Successfully" + self.result['response'] = claim_response + self.result['diff'] = self.validated_config + self.result['changed'] = True + else: + self.module.fail_json(msg="Device Claim Failed", response=[]) - if claim_response.get("response") == "Device Claimed": - self.result['changed'] = True - self.result['msg'] = "Device Claimed Successfully" - self.result['response'] = claim_response - self.result['diff'] = self.validated_config else: - self.module.fail_json(msg="Device Claim Failed", response=claim_response) + device_count_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='get_device_count', + op_modifies=True, + params=device_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: + claim_params = self.get_claim_params() + self.log(str(claim_params)) + claim_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='claim_a_device_to_a_site', + op_modifies=True, + params=claim_params, + ) + self.log(str(claim_response)) + if claim_response.get("response") == "Device Claimed": + self.result['msg'] = "Only Device Claimed Successfully" + self.result['response'] = claim_response + self.result['diff'] = self.validated_config + self.result['changed'] = True + else: + self.result['response'] = self.have.get("device_found") + self.result['msg'] = "Device is already claimed" return self diff --git a/tests/unit/modules/dnac/dnac_module.py b/tests/unit/modules/dnac/dnac_module.py index 85deb42ceb..0a8dbac3b6 100644 --- a/tests/unit/modules/dnac/dnac_module.py +++ b/tests/unit/modules/dnac/dnac_module.py @@ -78,12 +78,39 @@ def load_fixture(module_name, name, device=""): class TestDnacModule(ModuleTestCase): def __init__(self, module): + + """ + Initialize an instance of class . + + Parameters: + module (ModuleType): The Python module associated with this instance. + + Attributes: + module (ModuleType): The provided module. + test_data (dict): The loaded playbook data from the module. + playbook_config (dict): The playbook configuration. + playbook_config_missing_param (dict): The playbook configuration with missing parameters. + """ + self.module = module self.test_data = self.loadPlaybookData(str(self.module.__name__)) self.playbook_config = self.test_data.get("playbook_config") self.playbook_config_missing_param = self.test_data.get("playbook_config_missing_param") def setUp(self): + + """ + Set up the test environment by mocking Cisco DNA Center SDK initialization and execution. + This method is automatically called before each test case to ensure a clean and controlled environment. + Mocks the initialization and execution of the Cisco DNA Center SDK to isolate testing from actual SDK operations. + + Mocked attributes: + - mock_dnac_init: Mocks the initialization of the DNACSDK class. + - run_dnac_init: The started mock for DNACSDK initialization. + - mock_dnac_exec: Mocks the execution of DNACSDK methods. + - run_dnac_exec: The started mock for DNACSDK method execution. + """ + self.mock_dnac_init = patch( "ansible_collections.cisco.dnac.plugins.module_utils.dnac.DNACSDK.__init__") self.run_dnac_init = self.mock_dnac_init.start() @@ -94,17 +121,37 @@ def setUp(self): self.run_dnac_exec = self.mock_dnac_exec.start() def tearDown(self): + + """ + Clean up the test environment by stopping the mocked Cisco DNA Center SDK initialization and execution. + This method is automatically called after each test case to clean up any resources or mocks created during testing. + Stops the mock instances of the Cisco DNA Center SDK initialization and execution. + """ + self.mock_dnac_exec.stop() self.mock_dnac_init.stop() def loadPlaybookData(self, module): - path = os.path.join(fixture_path, "{0}.json".format(module)) - print(path) - with open(path) as f: - data = f.read() + """ + Load JSON data from a file. + + Parameters: + module (str): The name of the module used to construct the filename. + + Returns: + dict: The loaded JSON data. + + Raises: + FileNotFoundError: If the file does not exist. + json.JSONDecodeError: If there is an error decoding the JSON data. + """ + file_path = os.path.join(fixture_path, "{0}.json".format(module)) + print(file_path) try: + with open(file_path) as f: + data = f.read() j_data = json.loads(data) except Exception as e: print(e) @@ -115,6 +162,21 @@ def loadPlaybookData(self, module): def execute_module_devices( self, failed=False, changed=False, response=None, sort=True, defaults=False ): + + """ + This method executes a module for a single device. + + Parameters: + failed (bool, optional): If True, check for failures. Defaults to False. + changed (bool, optional): If True, check for changes. Defaults to False. + response (list, optional): The expected response data. Defaults to None. + sort (bool, optional): If True, sort the response data before comparison. Defaults to True. + device (str, optional): The device to execute the module on. Defaults to an empty string. + + Returns: + dict: A dictionary containing the execution result. + """ + module_name = self.module.__name__.rsplit(".", 1)[1] local_fixture_path = os.path.join(fixture_path, module_name) @@ -138,6 +200,22 @@ def execute_module( self, failed=False, changed=False, response=None, sort=True, device="" ): + """ + Execute a module for a specific device and perform validation. + + This method executes the module for a specific device, performs validation checks, and returns the result. + + Parameters: + failed (bool, optional): If True, check for failures. Defaults to False. + changed (bool, optional): If True, check for changes. Defaults to False. + response (list, optional): The expected response data. Defaults to None. + sort (bool, optional): If True, sort the response data before comparison. Defaults to True. + device (str, optional): The device to execute the module on. Defaults to an empty string. + + Returns: + dict: A dictionary containing the execution result, including 'failed', 'changed', and 'response' keys. + """ + self.load_fixtures(response, device=device) if failed: @@ -158,6 +236,14 @@ def execute_module( return result def failed(self): + + """ + Check for failures during module execution. + + Returns: + dict: A dictionary containing the failure status and additional information. + """ + with self.assertRaises(AnsibleFailJson) as exc: self.module.main() @@ -166,12 +252,20 @@ def failed(self): return result def changed(self, changed=False): + + """ + Check for changes during module execution. + + Parameters: + changed (bool, optional): If True, check for changes. Defaults to False. + + Returns: + dict: A dictionary containing the change status and additional information. + """ + with self.assertRaises(AnsibleExitJson) as exc: self.module.main() result = exc.exception.args[0] self.assertEqual(result["changed"], changed, result) return result - - def load_fixtures(self, response=None, device=""): - pass diff --git a/tests/unit/modules/dnac/test_pnp_intent.py b/tests/unit/modules/dnac/test_pnp_intent.py index af2ef09159..6f93e2a8ae 100644 --- a/tests/unit/modules/dnac/test_pnp_intent.py +++ b/tests/unit/modules/dnac/test_pnp_intent.py @@ -24,6 +24,7 @@ class TestDnacPnPIntent(TestDnacModule): def __init__(self): + """ Inheriting from the base class of dnac_module """ @@ -32,6 +33,15 @@ def __init__(self): 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 "site_not_found" in self._testMethodName: self.run_dnac_exec.side_effect = [ self.test_data.get("image_exists_response"), @@ -93,6 +103,13 @@ def load_fixtures(self, response=None, device=""): ] def test_pnp_intent_site_not_found(self): + + """ + Test case for PnP intent when site is not found. + + This test case checks the behavior of the PnP intent when the site is not found in the specified DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -110,6 +127,13 @@ def test_pnp_intent_site_not_found(self): ) def test_pnp_intent_add_new_device(self): + + """ + Test case for PnP intent when adding a new device. + + This test case checks the behavior of the PnP intent when adding a new device in the specified DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -127,6 +151,13 @@ def test_pnp_intent_add_new_device(self): ) def test_pnp_intent_device_exists(self): + + """ + Test case for PnP intent when a device already exists. + + This test case checks the behavior of the PnP intent when a device already exists in the specified DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -144,6 +175,13 @@ def test_pnp_intent_device_exists(self): ) def test_pnp_intent_image_doesnot_exist(self): + + """ + Test case for PnP intent when an image does not exist. + + This test case checks the behavior of the PnP intent when the specified image is not found in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -161,6 +199,13 @@ def test_pnp_intent_image_doesnot_exist(self): ) def test_pnp_intent_template_doesnot_exist(self): + + """ + Test case for PnP intent when a template does not exist. + + This test case checks the behavior of the PnP intent when the specified template is not found in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -178,6 +223,13 @@ def test_pnp_intent_template_doesnot_exist(self): ) def test_pnp_intent_project_not_found(self): + + """ + Test case for PnP intent when a project is not found. + + This test case checks the behavior of the PnP intent when the specified project is not found in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -195,6 +247,13 @@ def test_pnp_intent_project_not_found(self): ) def test_pnp_intent_missing_param(self): + + """ + Test case for PnP intent with missing parameters in the playbook. + + This test case checks the behavior of the PnP intent when the playbook contains missing required parameters. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -212,6 +271,13 @@ def test_pnp_intent_missing_param(self): ) def test_pnp_intent_delete_device(self): + + """ + Test case for PnP intent when deleting a device. + + This test case checks the behavior of the PnP intent when deleting a device in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -229,6 +295,13 @@ def test_pnp_intent_delete_device(self): ) def test_pnp_intent_deletion_error(self): + + """ + Test case for PnP intent when device deletion fails. + + This test case checks the behavior of the PnP intent when device deletion fails in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -246,6 +319,13 @@ def test_pnp_intent_deletion_error(self): ) def test_pnp_intent_delete_nonexisting_device(self): + + """ + Test case for PnP intent when deleting a non-existing device. + + This test case checks the behavior of the PnP intent when trying to delete a device that doesn't exist in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -263,6 +343,13 @@ def test_pnp_intent_delete_nonexisting_device(self): ) def test_pnp_intent_invalid_state(self): + + """ + Test case for PnP intent with an invalid state parameter. + + This test case checks the behavior of the PnP intent when an invalid 'state' parameter is provided in the playbook. + """ + set_module_args( dict( dnac_host="1.1.1.1", diff --git a/tests/unit/modules/dnac/test_site_intent.py b/tests/unit/modules/dnac/test_site_intent.py index 3de7357054..089d4ee85b 100644 --- a/tests/unit/modules/dnac/test_site_intent.py +++ b/tests/unit/modules/dnac/test_site_intent.py @@ -31,6 +31,15 @@ def __init__(self): 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_site" in self._testMethodName: self.run_dnac_exec.side_effect = [ Exception(), @@ -74,6 +83,13 @@ def load_fixtures(self, response=None, device=""): ] def test_site_intent_create_site(self): + + """ + Test case for site intent when creating a site. + + This test case checks the behavior of the site intent when creating a new site in the specified DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -91,6 +107,13 @@ def test_site_intent_create_site(self): ) def test_site_intent_update_not_needed(self): + + """ + Test case for site intent when no update is needed. + + This test case checks the behavior of the site intent when an update is not required for the specified site in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -108,6 +131,13 @@ def test_site_intent_update_not_needed(self): ) def test_site_intent_update_needed(self): + + """ + Test case for site intent when an update is needed. + + This test case checks the behavior of the site intent when an update is required for the specified site in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -125,6 +155,13 @@ def test_site_intent_update_needed(self): ) def test_site_intent_delete_existing_site(self): + + """ + Test case for site intent when deleting an existing site. + + This test case checks the behavior of the site intent when deleting an existing site in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -142,6 +179,13 @@ def test_site_intent_delete_existing_site(self): ) def test_site_intent_delete_non_existing_site(self): + + """ + Test case for site intent when attempting to delete a non-existing site. + + This test case checks the behavior of the site intent when trying to delete a site that does not exist in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -159,6 +203,13 @@ def test_site_intent_delete_non_existing_site(self): ) def test_site_intent_invalid_param(self): + + """ + Test case for site intent with invalid parameters in the playbook. + + This test case checks the behavior of the site intent when the playbook contains invalid parameters. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -175,6 +226,13 @@ def test_site_intent_invalid_param(self): ) def test_site_intent_error_delete(self): + + """ + Test case for site intent when an error occurs during site deletion. + + This test case checks the behavior of the site intent when an error occurs while deleting a site in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -192,6 +250,13 @@ def test_site_intent_error_delete(self): ) def test_site_intent_error_create(self): + + """ + Test case for site intent when an error occurs during site creation. + + This test case checks the behavior of the site intent when an error occurs while creating a site in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -210,6 +275,12 @@ def test_site_intent_error_create(self): def test_site_intent_invalid_state(self): + """ + Test case for site intent with an invalid 'state' parameter. + + This test case checks the behavior of the site intent when an invalid 'state' parameter is provided in the playbook. + """ + set_module_args( dict( dnac_host="1.1.1.1", diff --git a/tests/unit/modules/dnac/test_swim_intent.py b/tests/unit/modules/dnac/test_swim_intent.py index a9ef4fa33b..9c974a48fb 100644 --- a/tests/unit/modules/dnac/test_swim_intent.py +++ b/tests/unit/modules/dnac/test_swim_intent.py @@ -31,6 +31,15 @@ def __init__(self): 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 "full_flow" in self._testMethodName: self.run_dnac_exec.side_effect = [ self.test_data.get("task_info_response"), @@ -100,6 +109,13 @@ def load_fixtures(self, response=None, device=""): ] def test_swim_full_flow(self): + + """ + Test case for a full Software Image Management (SWIM) flow. + + This test case covers the full SWIM flow, including image activation, import, tagging, distribution, and various error scenarios. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -116,6 +132,13 @@ def test_swim_full_flow(self): ) def test_swim_image_import(self): + + """ + Test case for SWIM image import when the image already exists. + + This test case checks the behavior of SWIM when importing an image that already exists in the specified DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -132,6 +155,13 @@ def test_swim_image_import(self): ) def test_swim_image_local_import(self): + + """ + Test case for SWIM local image import when the image already exists. + + This test case checks the behavior of SWIM when importing a local image that already exists in the specified DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -148,6 +178,13 @@ def test_swim_image_local_import(self): ) def test_swim_untag_image(self): + + """ + Test case for SWIM untagging an image as Golden. + + This test case checks the behavior of SWIM when untagging an image as a Golden image in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -164,6 +201,13 @@ def test_swim_untag_image(self): ) def test_swim_missing_param_tag_golden_image(self): + + """ + Test case for SWIM with missing parameters for tagging a Golden image. + + This test case checks the behavior of SWIM when attempting to tag an image as Golden with missing parameters. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -180,6 +224,13 @@ def test_swim_missing_param_tag_golden_image(self): ) def test_swim_incorrect_site_untag_golden_image(self): + + """ + Test case for SWIM when trying to untag an image from a non-existing site. + + This test case checks the behavior of SWIM when attempting to untag an image from a non-existing site. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -196,6 +247,13 @@ def test_swim_incorrect_site_untag_golden_image(self): ) def test_swim_image_doesnot_exist_response(self): + + """ + Test case for SWIM when the image does not exist in the response. + + This test case checks the behavior of SWIM when the requested image is not found in the DNAC response. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -212,6 +270,13 @@ def test_swim_image_doesnot_exist_response(self): ) def test_swim_only_image_distribution(self): + + """ + Test case for SWIM with only image distribution. + + This test case checks the behavior of SWIM when distributing an image to devices. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -228,6 +293,13 @@ def test_swim_only_image_distribution(self): ) def test_swim_image_distribution_missing_param(self): + + """ + Test case for SWIM image distribution with missing parameters. + + This test case checks the behavior of SWIM when attempting to distribute an image with missing parameters. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -244,6 +316,13 @@ def test_swim_image_distribution_missing_param(self): ) def test_swim_only_image_activation(self): + + """ + Test case for SWIM with only image activation. + + This test case checks the behavior of SWIM when activating an image. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -260,6 +339,13 @@ def test_swim_only_image_activation(self): ) def test_swim_image_activation_missing_param(self): + + """ + Test case for SWIM image activation with missing parameters. + + This test case checks the behavior of SWIM when attempting to activate an image with missing parameters. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -276,6 +362,13 @@ def test_swim_image_activation_missing_param(self): ) def test_swim_tag_golden_incorrect_family_name(self): + + """ + Test case for SWIM when tagging an image as Golden with an incorrect family name. + + This test case checks the behavior of SWIM when attempting to tag an image as Golden with an incorrect family device name. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -292,6 +385,13 @@ def test_swim_tag_golden_incorrect_family_name(self): ) def test_swim_device_doesnot_exist(self): + + """ + Test case for SWIM when the device does not exist. + + This test case checks the behavior of SWIM when the specified device is not found in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -308,6 +408,13 @@ def test_swim_device_doesnot_exist(self): ) def test_swim_incorrect_image_import_parameter(self): + + """ + Test case for SWIM with incorrect image import parameters. + + This test case checks the behavior of SWIM when using incorrect image import parameters. + """ + set_module_args( dict( dnac_host="1.1.1.1", diff --git a/tests/unit/modules/dnac/test_template_intent.py b/tests/unit/modules/dnac/test_template_intent.py index b625118859..303ab63b12 100644 --- a/tests/unit/modules/dnac/test_template_intent.py +++ b/tests/unit/modules/dnac/test_template_intent.py @@ -31,6 +31,15 @@ def __init__(self): 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_template" in self._testMethodName: self.run_dnac_exec.side_effect = [ self.test_data.get("create_template_list_response"), @@ -70,6 +79,13 @@ def load_fixtures(self, response=None, device=""): ] def test_template_intent_create_template(self): + + """ + Test case for template intent when creating a template. + + This test case checks the behavior of the template intent when creating a new template in the specified DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -87,6 +103,13 @@ def test_template_intent_create_template(self): ) def test_template_intent_update_not_needed(self): + + """ + Test case for template intent when no update is needed. + + This test case checks the behavior of the template intent when an update is not required for the specified template in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -104,6 +127,13 @@ def test_template_intent_update_not_needed(self): ) def test_template_intent_update_needed(self): + + """ + Test case for template intent when an update is needed. + + This test case checks the behavior of the template intent when an update is required for the specified template in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -121,6 +151,13 @@ def test_template_intent_update_needed(self): ) def test_template_intent_project_not_found(self): + + """ + Test case for template intent when the project is not found. + + This test case checks the behavior of the template intent when the specified project is not found in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -138,6 +175,13 @@ def test_template_intent_project_not_found(self): ) def test_template_intent_delete_non_existing_template(self): + + """ + Test case for template intent when trying to delete a non-existing template. + + This test case checks the behavior of the template intent when trying to delete a template that does not exist in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -155,6 +199,13 @@ def test_template_intent_delete_non_existing_template(self): ) def test_template_intent_delete_template(self): + + """ + Test case for template intent when deleting a template. + + This test case checks the behavior of the template intent when deleting an existing template in the DNAC. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -172,6 +223,13 @@ def test_template_intent_delete_template(self): ) def test_template_intent_missing_param(self): + + """ + Test case for template intent with missing parameters in the playbook. + + This test case checks the behavior of the template intent when the playbook contains missing required parameters. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -189,6 +247,13 @@ def test_template_intent_missing_param(self): ) def test_template_intent_invalid_state(self): + + """ + Test case for template intent with an invalid 'state' parameter. + + This test case checks the behavior of the template intent when an invalid 'state' parameter is provided in the playbook. + """ + set_module_args( dict( dnac_host="1.1.1.1", @@ -206,6 +271,13 @@ def test_template_intent_invalid_state(self): ) def test_template_intent_invalid_param(self): + + """ + Test case for template intent with invalid parameters in the playbook. + + This test case checks the behavior of the template intent when the playbook contains invalid parameters. + """ + set_module_args( dict( dnac_host="1.1.1.1", From f93b26bf582770d7de518a0ddcc703f6da7758dd Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Tue, 7 Nov 2023 10:56:06 +0000 Subject: [PATCH 24/32] Removed whitespaces --- tests/unit/modules/dnac/test_swim_intent.py | 2 +- tests/unit/modules/dnac/test_template_intent.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/modules/dnac/test_swim_intent.py b/tests/unit/modules/dnac/test_swim_intent.py index 9c974a48fb..d2915e6218 100644 --- a/tests/unit/modules/dnac/test_swim_intent.py +++ b/tests/unit/modules/dnac/test_swim_intent.py @@ -414,7 +414,7 @@ def test_swim_incorrect_image_import_parameter(self): This test case checks the behavior of SWIM when using incorrect image import parameters. """ - + set_module_args( dict( dnac_host="1.1.1.1", diff --git a/tests/unit/modules/dnac/test_template_intent.py b/tests/unit/modules/dnac/test_template_intent.py index 303ab63b12..512d31a82d 100644 --- a/tests/unit/modules/dnac/test_template_intent.py +++ b/tests/unit/modules/dnac/test_template_intent.py @@ -277,7 +277,7 @@ def test_template_intent_invalid_param(self): This test case checks the behavior of the template intent when the playbook contains invalid parameters. """ - + set_module_args( dict( dnac_host="1.1.1.1", From 818615faebdfd7f1c92ded1631eb8373371b2f41 Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Wed, 8 Nov 2023 16:50:29 +0000 Subject: [PATCH 25/32] Resolved PR comments and have removed fail json for most else cases except exceptions --- plugins/modules/pnp_intent.py | 160 +++++++++++++------------ tests/unit/modules/dnac/dnac_module.py | 56 ++++----- 2 files changed, 114 insertions(+), 102 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index bb028cbade..d2b200becc 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -270,10 +270,10 @@ def validate_input(self): self.status = "success" return self - def site_exists(self): + def get_site_details(self): """ - Check whether the site exists or not + Check whether the site exists or not, along with side id Args: self: The instance of the class containing the 'config' attribute to be validated. Returns: @@ -472,10 +472,12 @@ def get_have(self): 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.module.fail_json(msg="Name of the site must be a string", response=[]) + self.msg = "Name of the site must be a string" + self.status = "failed" + return self site_name = self.want.get("site_name") - (site_exists, site_id) = self.site_exists() + (site_exists, site_id) = self.get_site_details() if site_exists: have["site_id"] = site_id @@ -483,8 +485,10 @@ def get_have(self): self.log("Site Name:" + str(site_name)) if self.want.get("pnp_type") == "AccessPoint": if self.get_site_type() != "floor": - self.module.fail_json(msg="Type of the site must \ - be a floor for claiming an AP", response=[]) + self.msg = "Type of the site must \ + be a floor for claiming an AP" + self.status = "failed" + return self if len(image_list) == 1: have["image_id"] = image_list[0].get("imageUuid") @@ -492,22 +496,25 @@ def get_have(self): template_name = self.want.get("template_name") if template_name: - if template_list and isinstance(template_list, list): - - template_details = get_dict_result(template_list, 'name', template_name) - - if template_details: - have["template_id"] = template_details.get("templateId") - else: - self.module.fail_json(msg="Template Not Found", response=[]) - + 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.module.fail_json(msg="Project Not Found \ - or Project is Empty", response=[]) + self.msg = "Template Not found" + self.status = "failed" + return self else: if not self.want.get('pnp_params').get('deviceInfo').get('add_device_method'): - self.module.fail_json(msg="Site not found", response=[]) + self.msg = "Either Site Name or Device addition method must be provided" + self.status = "failed" + return self # check if given device exists in pnp inventory, store device Id device_response = self.dnac_apply['exec']( @@ -564,18 +571,16 @@ def get_want(self, config): 'isSudiRequired': config.get('deviceInfo').get('isSudiRequired') } - if self.want["pnp_type"] != "Default": - - if self.want["pnp_type"] == "CatalystWLC": - self.want["staticIP"] = config.get('staticIP') - self.want["subnetMask"] = config.get('subnetMask') - self.want["gateway"] = config.get('gateway') - self.want["vlanId"] = config.get('vlanId') - self.want["ipInterfaceName"] = config.get('ipInterfaceName') + if self.want["pnp_type"] == "CatalystWLC": + self.want["staticIP"] = config.get('staticIP') + self.want["subnetMask"] = config.get('subnetMask') + 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") + elif self.want["pnp_type"] == "AccessPoint": + if self.get_site_type() == "floor": + self.want["rfProfile"] = config.get("rfProfile") self.msg = "Successfully collected all parameters from playbook " + \ "for comparison" @@ -607,58 +612,63 @@ class instance for further use. if not self.have.get("device_found"): if not self.want["add_device_method"]: - self.module.fail_json(msg="Device needs to be added before claiming", response=[]) - else: - 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.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.module.fail_json(msg="Device Addition Failed", response=[]) + self.msg = "Device needs to be added before claiming" + 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)) - claim_params = self.get_claim_params() - claim_params["deviceId"] = dev_add_response.get("id") - claim_response = self.dnac_apply['exec']( + 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='claim_a_device_to_a_site', + function="add_device", + params=self.want.get("pnp_params"), op_modifies=True, - params=claim_params, ) - self.log(str(claim_response)) - if claim_response.get("response") == "Device Claimed" \ - and self.have["deviceInfo"]: - self.result['msg'] = "Device Added and Claimed Successfully" - self.result['response'] = claim_response + 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.module.fail_json(msg="Device Claim Failed", response=[]) + 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)) + claim_params = self.get_claim_params() + claim_params["deviceId"] = dev_add_response.get("id") + claim_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function='claim_a_device_to_a_site', + op_modifies=True, + params=claim_params, + ) + + self.log(str(claim_response)) + if claim_response.get("response") == "Device Claimed" \ + and self.have["deviceInfo"]: + self.result['msg'] = "Device Added and Claimed Successfully" + self.result['response'] = claim_response + self.result['diff'] = self.validated_config + self.result['changed'] = True + else: + self.msg = "Device Claim Failed" + self.status = "failed" + return self else: device_count_response = self.dnac_apply['exec']( @@ -734,7 +744,9 @@ def get_diff_deleted(self): self.module.fail_json(msg=msg, response=response) else: - self.module.fail_json(msg="Device Not Found", response=[]) + self.msg = "Device Not Found" + self.status = "failed" + return self return self diff --git a/tests/unit/modules/dnac/dnac_module.py b/tests/unit/modules/dnac/dnac_module.py index 0a8dbac3b6..2a2ee78ced 100644 --- a/tests/unit/modules/dnac/dnac_module.py +++ b/tests/unit/modules/dnac/dnac_module.py @@ -83,13 +83,13 @@ def __init__(self, module): Initialize an instance of class . Parameters: - module (ModuleType): The Python module associated with this instance. + - module (ModuleType): The Python module associated with this instance. Attributes: - module (ModuleType): The provided module. - test_data (dict): The loaded playbook data from the module. - playbook_config (dict): The playbook configuration. - playbook_config_missing_param (dict): The playbook configuration with missing parameters. + - module (ModuleType): The provided module. + - test_data (dict): The loaded playbook data from the module. + - playbook_config (dict): The playbook configuration. + - playbook_config_missing_param (dict): The playbook configuration with missing parameters. """ self.module = module @@ -105,10 +105,10 @@ def setUp(self): Mocks the initialization and execution of the Cisco DNA Center SDK to isolate testing from actual SDK operations. Mocked attributes: - - mock_dnac_init: Mocks the initialization of the DNACSDK class. - - run_dnac_init: The started mock for DNACSDK initialization. - - mock_dnac_exec: Mocks the execution of DNACSDK methods. - - run_dnac_exec: The started mock for DNACSDK method execution. + - mock_dnac_init: Mocks the initialization of the DNACSDK class. + - run_dnac_init: The started mock for DNACSDK initialization. + - mock_dnac_exec: Mocks the execution of DNACSDK methods. + - run_dnac_exec: The started mock for DNACSDK method execution. """ self.mock_dnac_init = patch( @@ -137,14 +137,14 @@ def loadPlaybookData(self, module): Load JSON data from a file. Parameters: - module (str): The name of the module used to construct the filename. + - module (str): The name of the module used to construct the filename. Returns: - dict: The loaded JSON data. + - dict: The loaded JSON data. Raises: - FileNotFoundError: If the file does not exist. - json.JSONDecodeError: If there is an error decoding the JSON data. + - FileNotFoundError: If the file does not exist. + - json.JSONDecodeError: If there is an error decoding the JSON data. """ file_path = os.path.join(fixture_path, "{0}.json".format(module)) @@ -167,14 +167,14 @@ def execute_module_devices( This method executes a module for a single device. Parameters: - failed (bool, optional): If True, check for failures. Defaults to False. - changed (bool, optional): If True, check for changes. Defaults to False. - response (list, optional): The expected response data. Defaults to None. - sort (bool, optional): If True, sort the response data before comparison. Defaults to True. - device (str, optional): The device to execute the module on. Defaults to an empty string. + - failed (bool, optional): If True, check for failures. Defaults to False. + - changed (bool, optional): If True, check for changes. Defaults to False. + - response (list, optional): The expected response data. Defaults to None. + - sort (bool, optional): If True, sort the response data before comparison. Defaults to True. + - device (str, optional): The device to execute the module on. Defaults to an empty string. Returns: - dict: A dictionary containing the execution result. + - dict: A dictionary containing the execution result. """ module_name = self.module.__name__.rsplit(".", 1)[1] @@ -206,14 +206,14 @@ def execute_module( This method executes the module for a specific device, performs validation checks, and returns the result. Parameters: - failed (bool, optional): If True, check for failures. Defaults to False. - changed (bool, optional): If True, check for changes. Defaults to False. - response (list, optional): The expected response data. Defaults to None. - sort (bool, optional): If True, sort the response data before comparison. Defaults to True. - device (str, optional): The device to execute the module on. Defaults to an empty string. + - failed (bool, optional): If True, check for failures. Defaults to False. + - changed (bool, optional): If True, check for changes. Defaults to False. + - response (list, optional): The expected response data. Defaults to None. + - sort (bool, optional): If True, sort the response data before comparison. Defaults to True. + - device (str, optional): The device to execute the module on. Defaults to an empty string. Returns: - dict: A dictionary containing the execution result, including 'failed', 'changed', and 'response' keys. + - dict: A dictionary containing the execution result, including 'failed', 'changed', and 'response' keys. """ self.load_fixtures(response, device=device) @@ -241,7 +241,7 @@ def failed(self): Check for failures during module execution. Returns: - dict: A dictionary containing the failure status and additional information. + - dict: A dictionary containing the failure status and additional information. """ with self.assertRaises(AnsibleFailJson) as exc: @@ -257,10 +257,10 @@ def changed(self, changed=False): Check for changes during module execution. Parameters: - changed (bool, optional): If True, check for changes. Defaults to False. + - changed (bool, optional): If True, check for changes. Defaults to False. Returns: - dict: A dictionary containing the change status and additional information. + - dict: A dictionary containing the change status and additional information. """ with self.assertRaises(AnsibleExitJson) as exc: From 88cd7c457ef2549477c568a0949d04fdf7cb939b Mon Sep 17 00:00:00 2001 From: Abinash Mishra Date: Thu, 9 Nov 2023 06:17:59 +0000 Subject: [PATCH 26/32] Made a change in type --- plugins/modules/pnp_intent.py | 2 +- plugins/plugin_utils/dnac.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index d2b200becc..00f08cfd19 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -550,7 +550,7 @@ def get_want(self, config): Returns: The method returns an instance of the class with updated attributes: - self.want: A dictionary of paramters obtained from the playbook - - self.msgt: A message indicating all the paramters from the playbook are + - self.msg: A message indicating all the paramters from the playbook are collected - self.status: Success Example: diff --git a/plugins/plugin_utils/dnac.py b/plugins/plugin_utils/dnac.py index 80afbe39a6..7448c4c508 100644 --- a/plugins/plugin_utils/dnac.py +++ b/plugins/plugin_utils/dnac.py @@ -248,7 +248,7 @@ def exit_json(self): return self.result def verify_array(self, verify_interface, **kwargs): - if type(verify_interface) is None: + if verify_interface is None: return list() if isinstance(verify_interface, list): From 35d8e1c8e4ba812c190a216bcb8eaeadbdb81d3e Mon Sep 17 00:00:00 2001 From: Madhan Date: Fri, 10 Nov 2023 00:00:23 +0530 Subject: [PATCH 27/32] Alignments in API docstring --- plugins/modules/pnp_intent.py | 166 ++++++++++++++++++---------------- 1 file changed, 90 insertions(+), 76 deletions(-) diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 00f08cfd19..43bd474c79 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -211,22 +211,24 @@ 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. + 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. + - 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. + 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. + 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: @@ -275,14 +277,15 @@ def get_site_details(self): """ Check whether the site exists or not, along with side id Args: - 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: - - site_exits: A boolean value indicating the existence of the site. - - site_id: The Id of the site i.e. required to claim a device to site. + The method returns an instance of the class with updated attributes: + - site_exits: A boolean value indicating the existence of the site. + - site_id: The Id of the site i.e. required to claim device to site. Example: - Post creation of the validated input, we this method gets the site_id and checks - whether the site exists or not + Post creation of the validated input, we this method gets the + site_id and checks whether the site exists or not """ site_exists = False @@ -312,12 +315,15 @@ def get_site_type(self): """ Fetches the type of site Args: - 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: - - site_type: A string indicating the type of the site (area/building/floor). + 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 + Post creation of the validated input, we this method gets the + type of the site. """ try: @@ -344,16 +350,17 @@ def get_pnp_params(self, params): """ Store pnp parameters from the playbook for pnp processing in DNAC Args: - self: The instance of the class containing the 'config' attribute to be validated. - params: The validated params passed from the playbook + - self: The instance of the class containing the 'config' + attribute to be validated. + - params: The validated params passed from the playbook. Returns: - The method returns an instance of the class with updated attributes: - - pnp_params: A dictionary containing all the values indicating - the type of the site (area/building/floor). + The method returns an instance of the class with updated attributes: + - pnp_params: A dictionary containing all the values indicating + the type of the site (area/building/floor). 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 + Post creation of the validated input, it fetches the required paramters + and stores it for further processing and calling the parameters in + other APIs. """ pnp_params = { @@ -366,16 +373,17 @@ def get_image_params(self, params): """ Get image name and the confirmation whether it's tagged golden or not Args: - self: The instance of the class containing the 'config' attribute to be validated. - params: The validated params passed from the playbook + - self: The instance of the class containing the 'config' attribute + to be validated. + - params: The validated params passed from the playbook. Returns: - The method returns an instance of the class with updated attributes: - - image_params: A dictionary containing all the values indicating - name of the image and its golden image status. + The method returns an instance of the class with updated attributes: + - image_params: A dictionary containing all the values indicating + name of the image and its golden image status. 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 + Post creation of the validated input, it fetches the required + paramters and stores it for further processing and calling the + parameters in other APIs. """ image_params = { @@ -387,15 +395,17 @@ def get_image_params(self, params): def get_claim_params(self): """ - Get the paramters needed for claiming the device to site + Get the paramters needed for claiming the device to site. Args: - 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: - - claim_params: A dictionary needed for calling the POST call for claim - a device to a site API + The method returns an instance of the class with updated attributes: + - claim_params: A dictionary needed for calling the POST call + for claim a device to a site API. Example: - The stored dictionary can be used to call the API claim a device to a site via SDK + The stored dictionary can be used to call the API claim a device + to a site via SDK """ imageinfo = { @@ -438,15 +448,16 @@ def get_have(self): """ Get the current image, template and site details from the DNAC Args: - 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.image_response: A list of image passed by the user - - self.template_list: A list of template under project - - self.device_response: Gets the device_id and stores it + The method returns an instance of the class with updated attributes: + - self.image_response: A list of image passed by the user + - self.template_list: A list of template under project + - self.device_response: Gets the device_id and stores it Example: - 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 + 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 = {} @@ -543,19 +554,20 @@ def get_want(self, config): """ Get all the image, template and site and pnp related - information from playbook that is needed to be created in DNAC + information from playbook that is needed to be created in DNAC. Args: - self: The instance of the class containing the 'config' attribute to be validated. - config: validated config passed from the playbook + - 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 + 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 + It stores all the paramters passed from the playbook for further + processing before calling the APIs """ self.want = { @@ -591,18 +603,19 @@ def get_want(self, config): def get_diff_merged(self): """ - If given device doesnot exist - then add it to pnp database and get the device id + If given device doesnot exist then + add it to pnp database and get the device id 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. Returns: - object: An instance of the class with updated results and status - based on the processing of differences. + - 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. + 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. """ device_count_params = { @@ -709,13 +722,14 @@ 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. Returns: - self: An instance of the class with updated results and status based on - the deletion operation. + - 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. + This function is responsible for removing devices from the + Cisco DNA Center PnP GUI and raise Exception if any error occured. """ if self.have.get("device_found"): From 38f366b91b6b4130cf6687e6135a9450a4e6ea35 Mon Sep 17 00:00:00 2001 From: Madhan Date: Fri, 10 Nov 2023 01:24:12 +0530 Subject: [PATCH 28/32] docstring changes --- plugins/modules/network_settings_intent.py | 182 +++++++++++---------- plugins/modules/pnp_intent.py | 61 ++++--- 2 files changed, 124 insertions(+), 119 deletions(-) diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index fa38578daa..256c9c1b93 100644 --- a/plugins/modules/network_settings_intent.py +++ b/plugins/modules/network_settings_intent.py @@ -425,13 +425,12 @@ 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: - None - + - self: The instance of the class containing the 'config' attribute Returns: - self - + 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'). """ if not self.config: @@ -559,18 +558,18 @@ def requires_update(self, have, want, obj_params): from the playbook, using a specified schema for comparison. Parameters: - have (dict) - Current information from the Cisco DNA Center + - self: The instance of the class containing the 'config' attribute + - 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 @@ -585,13 +584,12 @@ 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: @@ -615,14 +613,16 @@ 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,14 +659,15 @@ 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 = { @@ -736,12 +737,13 @@ 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( @@ -839,13 +841,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 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. + 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. """ global_pool = { @@ -881,14 +883,18 @@ def reserve_pool_exists(self, name, site_name): Use check_return_status() to check for failure Parameters: - 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. + - name (str): Name of the Reserved pool to check for existence. + - site_name (str): Name of the site where Reserved pool is located. Returns: - 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. + 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. """ reserve_pool = { @@ -937,8 +943,7 @@ def get_have_global_pool(self, config): check this API using check_return_status. Parameters: - config (dict) - Playbook details containing Global Pool configuration. - + - config (dict): Playbook details containing Global Pool config. Returns: self - The current object with updated information. """ @@ -993,8 +998,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. """ @@ -1063,8 +1068,8 @@ 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. """ @@ -1091,15 +1096,16 @@ 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: @@ -1123,11 +1129,12 @@ 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 - The current object with updated desired Global Pool information. + - self: Current object with updated desired Global Pool information. """ # Initialize the desired Global Pool configuration @@ -1188,11 +1195,12 @@ 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 = { @@ -1300,11 +1308,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 - The current object with updated desired Network-related information. + - self: Current object with updated desired Network-related info. """ want_network = { @@ -1540,10 +1548,9 @@ 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: - None + - self """ if config.get("GlobalPoolDetails"): @@ -1569,10 +1576,10 @@ 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") \ @@ -1653,10 +1660,10 @@ 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") @@ -1739,10 +1746,10 @@ 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") @@ -1791,11 +1798,11 @@ 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: @@ -1814,10 +1821,10 @@ 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") @@ -1853,10 +1860,10 @@ 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") @@ -1886,13 +1893,14 @@ 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: @@ -1911,10 +1919,10 @@ def reset_values(self): Reset all neccessary attributes to default values Parameters: - None + - self Returns: - None + - None """ self.have.clear() diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 43bd474c79..a3c6232650 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -208,13 +208,13 @@ 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: + 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: @@ -273,10 +273,10 @@ def validate_input(self): return self def get_site_details(self): - """ Check whether the site exists or not, along with side id - Args: + + Parameters: - self: The instance of the class containing the 'config' attribute to be validated. Returns: @@ -311,10 +311,10 @@ def get_site_details(self): return (site_exists, site_id) def get_site_type(self): - """ Fetches the type of site - Args: + + Parameters: - self: The instance of the class containing the 'config' attribute to be validated. Returns: @@ -346,10 +346,10 @@ def get_site_type(self): return site_type def get_pnp_params(self, params): - """ - Store pnp parameters from the playbook for pnp processing in DNAC - Args: + Store pnp parameters from the playbook for pnp processing in DNAC. + + Parameters: - self: The instance of the class containing the 'config' attribute to be validated. - params: The validated params passed from the playbook. @@ -369,10 +369,10 @@ def get_pnp_params(self, params): return pnp_params def get_image_params(self, params): - """ Get image name and the confirmation whether it's tagged golden or not - Args: + + Parameters: - self: The instance of the class containing the 'config' attribute to be validated. - params: The validated params passed from the playbook. @@ -393,10 +393,9 @@ def get_image_params(self, params): return image_params def get_claim_params(self): - """ Get the paramters needed for claiming the device to site. - Args: + Parameters: - self: The instance of the class containing the 'config' attribute to be validated. Returns: @@ -444,10 +443,10 @@ def get_claim_params(self): return claim_params def get_have(self): - """ - Get the current image, template and site details from the DNAC - Args: + Get the current image, template and site details from the DNAC. + + Parameters: - self: The instance of the class containing the 'config' attribute to be validated. Returns: @@ -551,11 +550,11 @@ def get_have(self): return self def get_want(self, config): - """ Get all the image, template and site and pnp related information from playbook that is needed to be created in DNAC. - Args: + + Parameters: - self: The instance of the class containing the 'config' attribute to be validated. - config: validated config passed from the playbook @@ -601,11 +600,11 @@ def get_want(self, config): return self def get_diff_merged(self): - """ - If given device doesnot exist then - add it to pnp database and get the device id - Args: + If given device doesnot exist then add it to pnp database and + get the device id. + + Parameters: - self: An instance of a class used for interacting with Cisco DNA Center. Returns: @@ -716,12 +715,11 @@ class instance for further use. return self def get_diff_deleted(self): - """ - If the given device is added to pnp database - and is in unclaimed or failed state delete the - given device - Args: + If the given device is added to pnp database and is in unclaimed or + failed state delete the given device. + + Parameters: - self: An instance of a class used for interacting with Cisco DNA Center. Returns: @@ -766,7 +764,6 @@ def get_diff_deleted(self): def main(): - """ main entry point for module execution """ From 286c89fd4ed8e09eaef01d9f73f88636fd3ff131 Mon Sep 17 00:00:00 2001 From: Madhan Date: Fri, 10 Nov 2023 01:34:39 +0530 Subject: [PATCH 29/32] Adding new intent modules for credential, discovery and inventory --- changelogs/changelog.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index b67492ccf6..68bbab44ff 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -720,3 +720,12 @@ releases: minor_changes: - A new intent module for network settings to support Global IP Pool, Reserve IP Pool, Global servers, TimeZone, Message of the Day and telemetry servers. - By inheriting DNAC base class, changes done to Swim, Template, PnP intent modules. + 6.7.7: + release_date: "2023-11-10" + changes: + release_summary: Several changes to modules. + minor_changes: + - A new intent module to perform inventory for Adding, Deleting, Resyncing, Updating Devices etc. for all types of devices. + - A new intent module to Create, Update and Delete Global Device Credentials and Assign Credentials to a sites. + - A new intent module to discover the devices. + - Minor changes to swim intent module. From 67a67f8fbbc65f3ad3368782a30592868a9e2c7f Mon Sep 17 00:00:00 2001 From: Madhan Date: Fri, 10 Nov 2023 07:26:15 +0530 Subject: [PATCH 30/32] update in release version in changelog file --- changelogs/changelog.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 68bbab44ff..ff91d3176e 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -720,7 +720,7 @@ releases: minor_changes: - A new intent module for network settings to support Global IP Pool, Reserve IP Pool, Global servers, TimeZone, Message of the Day and telemetry servers. - By inheriting DNAC base class, changes done to Swim, Template, PnP intent modules. - 6.7.7: + 6.8.0: release_date: "2023-11-10" changes: release_summary: Several changes to modules. From b911ea178f7bcdd385f1eeed28363c3f5a5af8a4 Mon Sep 17 00:00:00 2001 From: Madhan Date: Sat, 11 Nov 2023 02:32:41 +0530 Subject: [PATCH 31/32] Updated revision --- plugins/modules/device_credential_intent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py index 320121ec52..01f9bc877c 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.7.0' +version_added: '6.8.0' extends_documentation_fragment: - cisco.dnac.intent_params author: Muthu Rakesh (@MUTHU-RAKESH-27) From a4e36982c8d73e1315a151005db40e74470b4e4a Mon Sep 17 00:00:00 2001 From: Madhan Date: Sat, 11 Nov 2023 02:36:23 +0530 Subject: [PATCH 32/32] update revision in inventory intent module --- plugins/modules/inventory_intent.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/modules/inventory_intent.py b/plugins/modules/inventory_intent.py index b6ad406718..33a8e4498c 100644 --- a/plugins/modules/inventory_intent.py +++ b/plugins/modules/inventory_intent.py @@ -18,10 +18,11 @@ - Adds the device with given credential. - Deletes the network device for the given Id. - Sync the devices provided as input. -version_added: '3.1.0' +version_added: '6.8.0' extends_documentation_fragment: - cisco.dnac.intent_params author: Abhishek Maheshwari (@abmahesh) + Madhan Sankaranarayanan (@madhansansel) options: state: description: The state of Cisco DNA Center after module completion.