diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index b67492ccf6..ff91d3176e 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.8.0: + 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. diff --git a/playbooks/PnP.yml b/playbooks/PnP.yml new file mode 100644 index 0000000000..be53564c03 --- /dev/null +++ b/playbooks/PnP.yml @@ -0,0 +1,95 @@ +--- +- name: Manage operations - Add, claim, and delete devices 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 only + cisco.dnac.pnp_intent: + <<: *dnac_login + dnac_log: True + state: merged + config: + - deviceInfo: + add_device_method: Single + serialNumber: FJC2330E0IK + hostname: Test-9300-6 + state: Unclaimed + pid: c9300-24P + isSudiRequired: True + + - 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: + add_device_method: Single + serialNumber: FJC2330E0IK + hostname: Test-9300-6 + state: Unclaimed + pid: c9300-24P + isSudiRequired: True + + - 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 + project_name: Onboarding Configuration + deviceInfo: + serialNumber: FJC271924EQ + hostname: Switch + state: Unclaimed + pid: C9300-48UXM + + - name: Claim an added Wireless Controller with template and image upgrade to a site only + cisco.dnac.pnp_intent: + <<: *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/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/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/module_utils/dnac.py b/plugins/module_utils/dnac.py index e4954ed513..98290e5dd1 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,21 +132,75 @@ 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 + 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. + validation_string (string) - String used to match the progress status. + + 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" + 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): """Reset all neccessary attributes to default values""" diff --git a/plugins/modules/device_credential_intent.py b/plugins/modules/device_credential_intent.py new file mode 100644 index 0000000000..01f9bc877c --- /dev/null +++ b/plugins/modules/device_credential_intent.py @@ -0,0 +1,2383 @@ +#!/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.8.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. Required for creating the credential. + type: str + enablePassword: + description: + - cliCredential credential Enable Password. + - Password cannot contain spaces or angle brackets (< >) + type: str + id: + description: Credential Id. Use this for updating the device credential. + type: str + password: + description: + - cliCredential credential Password. + - Required for creating/updating the credential. + - Password cannot contain spaces or angle brackets (< >). + type: str + username: + description: + - cliCredential credential Username. + - Username cannot contain spaces or angle brackets (< >). + type: str + old_description: + description: Old Description. Use this for updating the description/Username. + type: str + old_username: + description: Old Username. Use this for updating the description/Username. + type: str + httpsRead: + description: Global Credential V2's httpsRead. + type: list + elements: dict + suboptions: + id: + description: Credential Id. Use this for updating the device credential. + type: str + name: + description: Name. Required for creating the credential. + type: str + password: + description: + - httpsRead credential Password. + - Required for creating/updating the credential. + - Password cannot contain spaces or angle brackets (< >). + type: str + port: + description: Port. Default port is 443. + type: int + username: + description: + - httpsRead credential Username. + - Username cannot contain spaces or angle brackets (< >). + type: str + old_description: + description: Old Description. Use this for updating the description/Username. + type: str + old_username: + description: Old Username. Use this for updating the description/Username. + type: str + httpsWrite: + description: Global Credential V2's httpsWrite. + type: list + elements: dict + suboptions: + id: + description: Credential Id. Use this for updating the device credential. + type: str + name: + description: Name. Required for creating the credential. + type: str + password: + description: + - httpsWrite credential Password. + - Required for creating/updating the credential. + - Password cannot contain spaces or angle brackets (< >). + type: str + port: + description: Port. Default port is 443. + type: int + username: + description: + - httpsWrite credential Username. + - Username cannot contain spaces or angle brackets (< >). + type: str + old_description: + description: Old Description. Use this for updating the description/Username. + type: str + old_username: + description: Old Username. Use this for updating the description/Username. + type: str + snmpV2cRead: + description: Global Credential V2's snmpV2cRead. + type: list + elements: dict + suboptions: + description: + description: Description. Required for creating the credential. + type: str + id: + description: Credential Id. Use this for updating the device credential. + type: str + readCommunity: + description: + - snmpV2cRead Read Community. + - Password cannot contain spaces or angle brackets (< >). + type: str + old_description: + description: Old Description. Use this for updating the description. + type: str + snmpV2cWrite: + description: Global Credential V2's snmpV2cWrite. + type: list + elements: dict + suboptions: + description: + description: Description. Required for creating the credential. + type: str + id: + description: Credential Id. Use this for updating the device credential. + type: str + writeCommunity: + description: + - snmpV2cWrite Write Community. + - Password cannot contain spaces or angle brackets (< >). + type: str + old_description: + description: Old Description. Use this for updating the description. + type: str + snmpV3: + description: Global Credential V2's snmpV3. + type: list + elements: dict + suboptions: + authPassword: + description: + - snmpV3 Auth Password. + - Password must contain minimum 8 characters. + - Password cannot contain spaces or angle brackets (< >). + type: str + authType: + description: Auth Type. ["SHA", "MD5"]. + type: str + description: + description: + - snmpV3 Description. + - Should be unique from other snmpV3 credentials. + type: str + id: + description: Credential Id. Use this for updating the device credential. + type: str + privacyPassword: + description: + - snmpV3 Privacy Password. + - Password must contain minimum 8 characters. + - Password cannot contain spaces or angle brackets (< >). + type: str + privacyType: + description: Privacy Type. ["AES128", "AES192", "AES256"]. + type: str + snmpMode: + description: Snmp Mode. ["AUTHPRIV", "AUTHNOPRIV", "NOAUTHNOPRIV"]. + type: str + 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. + type: dict + suboptions: + cliDescription: + description: CLI Credential Description. + type: str + cliUsername: + description: CLI Credential Username. + type: str + cliId: + description: CLI Credential Id. Use (Description, Username) or 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. Use (Description, Username) or 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. Use (Description, Username) or 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. Use Description or Id. + type: str + snmpV2WriteDescription: + description: SNMPv2c Write Credential Description. + type: str + snmpV2WriteId: + description: SNMPv2c Write Credential Id. Use Description or Id. + type: str + snmpV3Description: + description: SNMPv3 Credential Description. + type: str + snmpV3Id: + description: SNMPv3 Credential Id. Use Description or 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. + """ + + 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): + """ + 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 = { + "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 = { + "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 = { + "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 = { + "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 = { + "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 = { + "username": item.get("username"), + "description": item.get("description"), + "snmpMode": item.get("snmpMode"), + "id": item.get("id"), + } + if value.get("snmpMode") == "AUTHNOPRIV": + value["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_cli_credentials(self, CredentialDetails, global_credentials): + """ + 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: + cliDetails (List) - The current CLI credentials. + """ + + # 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) + 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") + # 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) + 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") + # 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) + 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") + # 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) + 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") + # 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) + 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") + # 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) + 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) + 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: + self + + 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)) + 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": { + "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: + 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": { + "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)) + 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({ + "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: + self + + 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 + + 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( + family="network_settings", + function='assign_device_credential_to_site_v2', + params=credential_params, + ) + self.log(str(response)) + 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": { + "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: + config (dict) - Playbook details containing Global Device Credential information. + self - The current object details. + + Returns: + self + """ + + 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)) + 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 + + 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: + config (dict) - Playbook details containing Global Device Credential information. + self - The current object details. + + Returns: + self + """ + + 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: + self + + Returns: + self + """ + + self.have.clear() + self.want.clear() + return self + + +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/inventory_intent.py b/plugins/modules/inventory_intent.py new file mode 100644 index 0000000000..33a8e4498c --- /dev/null +++ b/plugins/modules/inventory_intent.py @@ -0,0 +1,869 @@ +#!/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: '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. + 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.Required for Adding Network Devices. + 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.Required for Adding Compute, Meraki, Firepower Management Devices. + type: str + httpPort: + 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 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/Resyncing Device except Meraki Devices. + 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.Required for Adding Network Device. + type: str + serialNumber: + description: Network Device's serialNumber. + type: str + snmpAuthPassphrase: + description: Network Device's snmpAuthPassphrase.Required for Adding Network, Compute, Third Party Devices. + 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.Required for Adding Network, Compute, Third Party Devices. + type: str + snmpPrivProtocol: + 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 Devices. + type: str + default: public + snmpRWCommunity: + description: Network Device's snmpRWCommunity.Required for Adding V2C Devices. + 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.Required for Adding Network, Compute, Third Party Devices. + 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.Required for Adding Network Device. + 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 + deviceResync: false + +- name: Add new Compute device in Inventory with full credentials.Inputs needed for Compute 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 + deviceResync: 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 + deviceResync: 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 + deviceResync: 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 + deviceResync: 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 + deviceResync: True + forceSync: False + +- 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 DNA Center 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 Inventory 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. + 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'}, + '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'}, + 'deviceResync': {'type': 'bool'}, + 'forceSync': {'type': 'bool'} + } + + # Validate device 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 + 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 Cisco DNA Center and return both device_exist and device_not_exist in dnac. + Args: + want_device (list): A list of devices you want to check for existence in Cisco DNA Center. + Returns: + list: A list of devices that exist in Cisco DNA Center. + Description: + Queries Cisco DNA Center to check which devices from 'want_device' are already present. If a device is found + in Cisco DNA Center, its management IP address is included in the list of devices that exist. + 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 = [] + + try: + response = self.dnac._exec( + family="devices", + function='get_device_list', + ) + + except Exception as e: + error_message = "Error while fetching device from Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) + raise Exception(error_message) + + if response: + 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 mandatory_parameter(self, config): + """ + Check for and validate mandatory parameters for adding network devices in Cisco DNA Center. + Args: + config (dict): A dictionary containing the configuration details for adding a network device to Cisco DNA Center. + Returns: + dict: The input `config` dictionary if all mandatory parameters are present. + Description: + It will check the mandatory parameters for adding the devices in Cisco DNA Center. + """ + + 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 = [] + 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 + + def get_have(self, config): + """ + 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 + Cisco DNA Center, and devices that are not present in Cisco DNA Center. + Description: + 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 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 Cisco DNA Center + 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) + + 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 + + self.have = have + + return self + + def get_device_params(self, params): + """ + 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 Cisco DNA Center. + """ + + device_param = { + "cli_transport": params.get("cliTransport"), + "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"), + "force_sync": params.get("forceSync") + } + + return device_param + + def get_device_ids(self, device_ips): + """ + Get the list of unique device IDs for list of specified management IP addresses of devices in Cisco DNA Center. + Args: + self (object): An instance of a class used for interacting with Cisco DNA Center. + device_ips (list): The management IP addresses of devices for which you want to retrieve the device IDs. + Returns: + 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. + """ + + 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: + self.log(str(response)) + response = response.get("response")[0] + device_id = response["id"] + device_ids.append(device_id) + + except Exception as e: + error_message = "Error while fetching device from Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) + raise Exception(error_message) + + return device_ids + + def get_want(self, config): + """ + Get all the device related information from playbook that is needed to be + add/update/delete/resync device in Cisco DNA Center. + Args: + 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: + Retrieve all the device-related information from the playbook needed for adding, updating, deleting, + or resyncing devices in Cisco DNA Center. + """ + + want = {} + device_params = self.get_device_params(config) + want["device_params"] = device_params + + self.want = want + self.msg = "Successfully collected all parameters from the playbook " + self.status = "success" + + return self + + def get_diff_merged(self, config): + """ + 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 Cisco DNA Center. + The updated results and status are stored in the class instance for further use. + """ + + device_added = False + device_updated = False + + devices_to_add = self.have["device_not_in_dnac"] + device_type = self.config[0].get("type", "NETWORK_DEVICE") + device_resynced = self.config[0].get("deviceResync", "False") + self.result['log'] = [] + + if device_resynced: + # 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("forceSync", "False") + resync_param_dict = { + 'payload': device_ids, + 'force_sync': force_sync + } + response = self.dnac._exec( + family="devices", + function='sync_devices_using_forcesync', + op_modifies=True, + params=resync_param_dict, + ) + self.log(str(response)) + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + while True: + execution_details = self.get_task_details(task_id) + + if 'Synced' in execution_details.get("progress"): + self.status = "success" + self.result['changed'] = True + self.result['response'] = execution_details + break + elif execution_details.get("isError") and execution_details.get("failureReason"): + self.msg = "Device Resynced get failed because of {0}".format(execution_details.get("failureReason")) + self.status = "failed" + break + self.log("Device Resynced Successfully") + self.log("Resynced devices are :" + str(device_ips)) + msg = "Device " + str(device_ips) + " Resynced Successfully !!" + self.result['log'].append(msg) + + return self + + except Exception as e: + error_message = "Error while Resyncing device in Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) + raise Exception(error_message) + + if not devices_to_add: + # Write code for device updation + device_updated = True + + log("Devices {0} are present in Cisco DNA Center and updated successfully".format(config['ipAddress'])) + msg = "Devices {0} present in Cisco DNA Center and updated successfully".format(config['ipAddress']) + self.result['log'].append(msg) + self.status = "success" + return self + + # If we want to add device in inventory + self.mandatory_parameter(config).check_return_status() + config['ipAddress'] = devices_to_add + config['type'] = device_type + if device_type == "FIREPOWER_MANAGEMENT_SYSTEM": + config['httpPort'] = self.config[0].get("httpPort", "443") + + try: + response = self.dnac._exec( + family="devices", + function='add_device', + op_modifies=True, + params=config, + ) + + log(str(response)) + device_added = True + + 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: + error_message = "Error while Adding device in Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) + raise Exception(error_message) + + return self + + def get_diff_deleted(self, config): + """ + 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 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 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_ids([device_ip]) + try: + response = self.dnac._exec( + family="devices", + function='delete_device_by_id', + params={"id": device_id[0]}, + ) + + if response and isinstance(response, dict): + task_id = response.get('response').get('taskId') + while True: + execution_details = self.get_task_details(task_id) + + if 'success' in execution_details.get("progress"): + self.msg = "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.msg = "Device Deletion get failed because of {0}".format(execution_details.get("failureReason")) + self.status = "failed" + break + + except Exception as e: + error_message = "Error while Deleting device from Cisco DNA Center - {0}".format(str(e)) + self.log(error_message) + raise Exception(error_message) + + return self + + +def main(): + """ main entry point for module execution + """ + + 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}, + '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() diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py index b00852247e..256c9c1b93 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: > @@ -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: @@ -555,22 +554,22 @@ 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 + - 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 - DNAC 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 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 - + - 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. + - 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: @@ -658,14 +658,16 @@ 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 + - 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 + - reserve_pool (dict): Processed Reserved pool data in the format + suitable for the Cisco DNA Center config. """ reserve_pool = { @@ -731,14 +733,17 @@ 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 + - 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 +772,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": { @@ -836,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 = { @@ -878,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 = { @@ -930,12 +939,11 @@ 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: - config (dict) - Playbook details containing Global Pool configuration. - + - config (dict): Playbook details containing Global Pool config. Returns: self - The current object with updated information. """ @@ -979,19 +987,19 @@ 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 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. """ @@ -1015,7 +1023,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 +1039,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,17 +1058,18 @@ 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. - + config (dict) - Playbook details containing Network Management + configuration. Returns: self - The current object with updated Network information. """ @@ -1078,23 +1088,24 @@ 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, - 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: @@ -1106,8 +1117,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 @@ -1118,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 @@ -1183,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 = { @@ -1295,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 = { @@ -1535,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"): @@ -1561,13 +1573,13 @@ 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 + - config (list of dict): Playbook details Returns: - None + - None """ name = config.get("GlobalPoolDetails") \ @@ -1599,7 +1611,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,21 +1655,21 @@ 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: - 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") 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 +1716,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 +1725,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,13 +1742,14 @@ 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. - + - config (list of dict) - Playbook details containing Network + Management information. Returns: - None + - None """ siteName = config.get("NetworkManagementDetails").get("siteName") @@ -1753,13 +1766,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,14 +1795,14 @@ 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 - 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: @@ -1804,13 +1818,13 @@ 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. + - name (str): The name of the Reserve Pool to be deleted. Returns: - self + - self """ reserve_pool_exists = self.have.get("reservePool").get("exists") @@ -1843,13 +1857,13 @@ 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. + - name (str): The name of the Global Pool to be deleted. Returns: - self + - self """ global_pool_exists = self.have.get("globalPool").get("exists") @@ -1879,13 +1893,14 @@ 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 + - config (list of dict): Playbook details Returns: - self + - self """ if config.get("ReservePoolDetails") is not None: @@ -1904,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 6ce4a54256..a3c6232650 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. @@ -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. - 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. + add_device_method: + description: Pnp Device's device addition method (Single/Bulk/Smart Account). 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, @@ -622,9 +208,27 @@ def __init__(self, module): super().__init__(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. + + Parameters: + - self: The instance of the class containing the 'config' attribute + to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - self.msg: A message describing the validation result. + - self.status: The status of the validation (either 'success' or 'failed'). + - self.validated_config: If successful, a validated version of the + 'config' parameter. + Example: + To use this method, create an instance of the class and call + 'validate_input' on it.If the validation succeeds, 'self.status' + will be 'success'and 'self.validated_config' will contain the + validated configuration. If it fails, 'self.status' will be + 'failed', and 'self.msg' will describe the validation issues. """ if not self.config: @@ -633,14 +237,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 +267,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): - + def get_site_details(self): """ - Check whether the site exists or not + Check whether the site exists or not, along with side id + + Parameters: + - self: The instance of the class containing the 'config' + attribute to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - site_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 """ site_exists = False @@ -682,34 +304,86 @@ 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) - def get_pnp_params(self, params): + def get_site_type(self): + """ + Fetches the type of site + + Parameters: + - self: The instance of the class containing the 'config' attribute + to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - site_type: A string indicating the type of the + site (area/building/floor). + Example: + Post creation of the validated input, we this method gets the + type of the site. + """ + + try: + response = self.dnac_apply['exec']( + family="sites", + function='get_site', + params={"name": 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 + 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. + 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 = { - '_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 def get_image_params(self, params): - """ Get image name and the confirmation whether it's tagged golden or not + + Parameters: + - 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 = { @@ -719,9 +393,18 @@ def get_image_params(self, params): return image_params def get_claim_params(self): - """ - Get the paramters needed for claiming + Get the paramters needed for claiming the device to site. + Parameters: + - self: The instance of the class containing the 'config' + attribute to be validated. + Returns: + The method returns an instance of the class with updated attributes: + - 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 +430,33 @@ 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 + 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: + 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 +467,64 @@ 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 not isinstance(self.want.get("site_name"), str) and \ + not self.want.get('pnp_params').get('deviceInfo').get('add_device_method'): + self.msg = "Name of the site must be a string" + self.status = "failed" + return self + + site_name = self.want.get("site_name") + (site_exists, site_id) = self.get_site_details() if site_exists: have["site_id"] = site_id self.log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) self.log("Site Name:" + str(site_name)) + if self.want.get("pnp_type") == "AccessPoint": + if self.get_site_type() != "floor": + self.msg = "Type of the site must \ + be a floor for claiming an AP" + self.status = "failed" + return self + + if len(image_list) == 1: + have["image_id"] = image_list[0].get("imageUuid") + self.log("Image Id: " + str(have["image_id"])) + + template_name = self.want.get("template_name") + if template_name: + if not (template_list and isinstance(template_list, list)): + self.msg = "Project Not Found \ + or Project is Empty" + self.status = "failed" + return self + + template_details = get_dict_result(template_list, 'name', template_name) + if template_details: + have["template_id"] = template_details.get("templateId") + else: + self.msg = "Template Not found" + self.status = "failed" + return self + + else: + if not self.want.get('pnp_params').get('deviceInfo').get('add_device_method'): + self.msg = "Either Site Name or Device addition method must be provided" + self.status = "failed" + return self # check if given device exists in pnp inventory, store device Id device_response = self.dnac_apply['exec']( @@ -831,10 +550,23 @@ def get_have(self): return self def get_want(self, config): - """ - Get all the image, site and pnp related - information from playbook that is needed to be created in DNAC + Get all the image, template and site and pnp related + information from playbook that is needed to be created in DNAC. + + Parameters: + - self: The instance of the class containing the 'config' + attribute to be validated. + - config: validated config passed from the playbook + Returns: + The method returns an instance of the class with updated attributes: + - self.want: A dictionary of paramters obtained from the playbook. + - self.msg: A message indicating all the paramters from the playbook + are collected. + - self.status: Success. + Example: + It stores all the paramters passed from the playbook for further + processing before calling the APIs """ self.want = { @@ -845,9 +577,22 @@ 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') + + 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" self.status = "success" @@ -855,48 +600,137 @@ 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 + 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: + - 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"): - 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") + device_count_params = { + "serial_number": self.want.get("serial_number"), + "state": "Provisioned" + } - self.log(str(response)) - self.log(self.have.get("device_id")) + if not self.have.get("device_found"): + if not self.want["add_device_method"]: + self.msg = "Device needs to be added before claiming" + self.status = "failed" + return self + + if not self.want["site_name"]: + if self.want["add_device_method"] == "Single": + self.log("Adding device to pnp database") + dev_add_response = self.dnac_apply['exec']( + family="device_onboarding_pnp", + function="add_device", + params=self.want.get("pnp_params"), + op_modifies=True, + ) + + self.have["deviceInfo"] = dev_add_response.get("deviceInfo") + self.log(str(dev_add_response)) + if self.have["deviceInfo"]: + self.result['msg'] = "Only Device Added Successfully" + self.result['response'] = dev_add_response + self.result['diff'] = self.validated_config + self.result['changed'] = True + else: + self.msg = "Device Addition Failed" + self.status = "failed" + return self - 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, - ) + 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)) + 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 - 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 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 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. + + Parameters: + - 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 +744,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 @@ -922,13 +756,14 @@ 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 def main(): - """ main entry point for module execution """ diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index 91360148ff..4c1b04f143 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -724,3 +724,7 @@ 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 +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 36f702f1b3..2ee339e701 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -1075,3 +1075,7 @@ 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 +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 a6ce8c2866..98bf9eed14 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -22,3 +22,7 @@ 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 +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 a0cfbc0671..0df7244629 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 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 a0cfbc0671..0df7244629 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 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 a0cfbc0671..0df7244629 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 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 91360148ff..4c1b04f143 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -724,3 +724,7 @@ 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 +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/unit/modules/dnac/dnac_module.py b/tests/unit/modules/dnac/dnac_module.py index c05b5a6eed..2a2ee78ced 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,9 +76,107 @@ 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() + 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): + + """ + 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): + + """ + 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) + pass + + return j_data + 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) @@ -101,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: @@ -121,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() @@ -129,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 b3c15a4ff4..6f93e2a8ae 100644 --- a/tests/unit/modules/dnac/test_pnp_intent.py +++ b/tests/unit/modules/dnac/test_pnp_intent.py @@ -17,39 +17,31 @@ __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): - module = pnp_intent + """ + Inheriting from the base class of dnac_module + """ - test_data = loadPlaybookData("pnp_intent") + module = pnp_intent + super().__init__(module) - 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() + def load_fixtures(self, response=None, device=""): - 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() + """ + Load fixtures for a specific device. - def tearDown(self): - super(TestDnacPnPIntent, self).tearDown() - self.mock_dnac_exec.stop() - self.mock_dnac_init.stop() + 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. + """ - def load_fixtures(self, response=None, device=""): if "site_not_found" in self._testMethodName: self.run_dnac_exec.side_effect = [ self.test_data.get("image_exists_response"), @@ -111,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", @@ -128,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", @@ -145,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", @@ -162,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", @@ -179,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", @@ -196,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", @@ -213,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", @@ -230,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", @@ -247,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", @@ -264,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", @@ -281,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 a12f1d1c44..089d4ee85b 100644 --- a/tests/unit/modules/dnac/test_site_intent.py +++ b/tests/unit/modules/dnac/test_site_intent.py @@ -17,39 +17,29 @@ __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") + module = site_intent + super().__init__(module) - 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() + def load_fixtures(self, response=None, device=""): - 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() + """ + Load fixtures for a specific device. - def tearDown(self): - super(TestDnacSiteIntent, self).tearDown() - self.mock_dnac_exec.stop() - self.mock_dnac_init.stop() + 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. + """ - def load_fixtures(self, response=None, device=""): if "create_site" in self._testMethodName: self.run_dnac_exec.side_effect = [ Exception(), @@ -93,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", @@ -110,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", @@ -127,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", @@ -144,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", @@ -161,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", @@ -178,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", @@ -194,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", @@ -211,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", @@ -229,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 18acf4762f..d2915e6218 100644 --- a/tests/unit/modules/dnac/test_swim_intent.py +++ b/tests/unit/modules/dnac/test_swim_intent.py @@ -17,37 +17,29 @@ __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") + module = swim_intent + super().__init__(module) - def setUp(self): - super(TestDnacSwimIntent, self).setUp() + def load_fixtures(self, response=None, device=""): - 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() + """ + Load fixtures for a specific device. - def tearDown(self): - super(TestDnacSwimIntent, self).tearDown() - self.mock_dnac_exec.stop() - self.mock_dnac_init.stop() + 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. + """ - def load_fixtures(self, response=None, device=""): if "full_flow" in self._testMethodName: self.run_dnac_exec.side_effect = [ self.test_data.get("task_info_response"), @@ -117,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", @@ -133,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", @@ -149,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", @@ -165,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", @@ -181,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", @@ -197,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", @@ -213,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", @@ -229,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", @@ -245,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", @@ -261,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", @@ -277,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", @@ -293,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", @@ -309,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", @@ -325,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 c0f866d460..512d31a82d 100644 --- a/tests/unit/modules/dnac/test_template_intent.py +++ b/tests/unit/modules/dnac/test_template_intent.py @@ -17,37 +17,29 @@ __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 + module = template_intent + super().__init__(module) - test_data = loadPlaybookData("template_intent") - - playbook_config = test_data.get("playbook_config") - playbook_config_missing_param = test_data.get("playbook_config_missing_param") + def load_fixtures(self, response=None, device=""): - 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() + """ + Load fixtures for a specific device. - def tearDown(self): - super(TestDnacTemplateIntent, self).tearDown() - self.mock_dnac_exec.stop() - self.mock_dnac_init.stop() + 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. + """ - def load_fixtures(self, response=None, device=""): if "create_template" in self._testMethodName: self.run_dnac_exec.side_effect = [ self.test_data.get("create_template_list_response"), @@ -87,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", @@ -104,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", @@ -121,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", @@ -138,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", @@ -155,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", @@ -172,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", @@ -189,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", @@ -206,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", @@ -223,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",