diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 8f16937e35..b67492ccf6 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -712,4 +712,11 @@ releases: - Updated from get_permissions_ap_i to get_permissions_api - Updated from get_roles_ap_i to get_roles_api - Updated from get_users_ap_i to get_users_api - - Updated from get_external_authentication_servers_ap_i to get_external_authentication_servers_api \ No newline at end of file + - Updated from get_external_authentication_servers_ap_i to get_external_authentication_servers_api + 6.7.6: + release_date: "2023-10-13" + changes: + release_summary: Several changes to modules. + 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. diff --git a/galaxy.yml b/galaxy.yml index 07683c4ad4..ec507d4ce2 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,7 +1,7 @@ --- namespace: cisco name: dnac -version: 6.7.5 +version: 6.7.6 readme: README.md authors: - Rafael Campos diff --git a/playbooks/network_settings_intent.yml b/playbooks/network_settings_intent.yml new file mode 100644 index 0000000000..58220da335 --- /dev/null +++ b/playbooks/network_settings_intent.yml @@ -0,0 +1,86 @@ +- hosts: dnac_servers + vars_files: + - credentials_245.yml + gather_facts: no + connection: local + tasks: +# +# Project Info Section +# + + - name: Create global pool, reserve subpool and network functions + cisco.dnac.network_settings_intent: + dnac_host: "{{ dnac_host }}" + dnac_port: "{{ dnac_port }}" + dnac_username: "{{ dnac_username }}" + dnac_password: "{{ dnac_password }}" + dnac_verify: "{{ dnac_verify }}" + dnac_debug: "{{ dnac_debug }}" + dnac_log: True + state: merged + config: + - GlobalPoolDetails: + settings: + ippool: + - ipPoolName: Global_Pool2 + gateway: "" #use this for updating + IpAddressSpace: IPv6 #required when we are creating + ipPoolCidr: 2001:db8::/64 #required when we are creating + type: Generic + dhcpServerIps: [] #use this for updating + dnsServerIps: [] #use this for updating + # prev_name: Global_Pool2 + ReservePoolDetails: + ipv6AddressSpace: True + ipv4GlobalPool: 100.0.0.0/8 + ipv4Prefix: True + ipv4PrefixLength: 9 + ipv4Subnet: 100.128.0.0 + # ipv4DnsServers: [100.128.0.1] + name: IP_Pool_3 + ipv6Prefix: True + ipv6PrefixLength: 64 + ipv6GlobalPool: 2001:db8::/64 + ipv6Subnet: "2001:db8::" + siteName: Global/Chennai/Trill + slaacSupport: True + # prev_name: IP_Pool_4 + type: LAN + NetworkManagementDetails: + settings: + dhcpServer: + - 10.0.0.1 + dnsServer: + domainName: cisco.com + primaryIpAddress: 10.0.0.2 + secondaryIpAddress: 10.0.0.3 + clientAndEndpoint_aaa: #works only if we system settigns is set + # ipAddress: 10.197.156.42 #Mandatory for ISE, sec ip for AAA + network: 10.0.0.20 + protocol: RADIUS + servers: AAA + # sharedSecret: string #ISE + messageOfTheday: + bannerMessage: hello + retainExistingBanner: "true" + netflowcollector: + ipAddress: 10.0.0.4 + port: 443 + network_aaa: #works only if we system settigns is set + # ipAddress: string #Mandatory for ISE, sec ip for AAA + network: 10.0.0.20 + protocol: TACACS + servers: AAA + # sharedSecret: string #ISE + ntpServer: + - 10.0.0.5 + snmpServer: + configureDnacIP: True + ipAddresses: + - 10.0.0.6 + syslogServer: + configureDnacIP: True + ipAddresses: + - 10.0.0.7 + timezone: GMT + siteName: Global/Chennai diff --git a/plugins/module_utils/dnac.py b/plugins/module_utils/dnac.py index 46f157e879..e4954ed513 100644 --- a/plugins/module_utils/dnac.py +++ b/plugins/module_utils/dnac.py @@ -14,6 +14,7 @@ DNAC_SDK_IS_INSTALLED = True from ansible.module_utils._text import to_native from ansible.module_utils.common import validation +from abc import ABCMeta, abstractmethod try: import logging except ImportError: @@ -26,14 +27,18 @@ import inspect -class DnacBase: +class DnacBase(): + """Class contains members which can be reused for all intent modules""" + + __metaclass__ = ABCMeta + def __init__(self, module): self.module = module self.params = module.params self.config = copy.deepcopy(module.params.get("config")) - self.have_create = {} - self.want_create = {} + self.have = {} + self.want = {} self.validated_config = [] self.msg = "" self.status = "success" @@ -41,12 +46,57 @@ def __init__(self, module): self.dnac = DNACSDK(params=dnac_params) self.dnac_apply = {'exec': self.dnac._exec} self.get_diff_state_apply = {'merged': self.get_diff_merged, - 'deleted': self.get_diff_deleted} + 'deleted': self.get_diff_deleted, + 'replaced': self.get_diff_replaced, + 'overridden': self.get_diff_overridden, + 'gathered': self.get_diff_gathered, + 'rendered': self.get_diff_rendered, + 'parsed': self.get_diff_parsed + } self.dnac_log = dnac_params.get("dnac_log") - self.log(str(dnac_params)) - self.supported_states = ["merged", "deleted"] + log(str(dnac_params)) + self.supported_states = ["merged", "deleted", "replaced", "overridden", "gathered", "rendered", "parsed"] self.result = {"changed": False, "diff": [], "response": [], "warnings": []} + @abstractmethod + def validate_input(self): + pass + + def get_diff_merged(self): + # Implement logic to merge the resource configuration + self.merged = True + return self + + def get_diff_deleted(self): + # Implement logic to delete the resource + self.deleted = True + return self + + def get_diff_replaced(self): + # Implement logic to replace the resource + self.replaced = True + return self + + def get_diff_overridden(self): + # Implement logic to overwrite the resource + self.overridden = True + return self + + def get_diff_gathered(self): + # Implement logic to gather data about the resource + self.gathered = True + return self + + def get_diff_rendered(self): + # Implement logic to render a configuration template + self.rendered = True + return self + + def get_diff_parsed(self): + # Implement logic to parse a configuration file + self.parsed = True + return True + def log(self, message, frameIncrement=0): """Log messages into dnac.log file""" @@ -97,8 +147,66 @@ def get_task_details(self, task_id): def reset_values(self): """Reset all neccessary attributes to default values""" - self.have_create.clear() - self.want_create.clear() + self.have.clear() + self.want.clear() + + def get_execution_details(self, execid): + """ + Get the execution details of an API + + Parameters: + execid (str) - Id for API execution + + Returns: + response (dict) - Status for API execution + """ + + self.log("Execution Id " + str(execid)) + response = self.dnac._exec( + family="task", + function='get_business_api_execution_details', + params={"execution_id": execid} + ) + self.log("Response for the current execution" + str(response)) + return response + + def check_execution_response_status(self, response): + """ + Checks the reponse status provided by API in the DNAC + + Parameters: + response (dict) - API response + + Returns: + self + """ + + self.log(str(response)) + if not response: + self.msg = "response is empty" + self.status = "failed" + return self + + if not isinstance(response, dict): + self.msg = "response is not a dictionary" + self.status = "failed" + return self + + executionid = response.get("executionId") + while True: + execution_details = self.get_execution_details(executionid) + if execution_details.get("status") == "SUCCESS": + self.result['changed'] = True + self.msg = "Successfully executed" + self.status = "success" + break + + if execution_details.get("bapiError"): + self.msg = execution_details.get("bapiError") + self.status = "failed" + break + + return self def log(msg, frameIncrement=0): @@ -203,6 +311,167 @@ def dnac_argument_spec(): return argument_spec +def validate_str(item, param_spec, param_name, invalid_params): + """ + This function checks that the input `item` is a valid string and confirms to + the constraints specified in `param_spec`. If the string is not valid or does + not meet the constraints, an error message is added to `invalid_params`. + + Args: + item (str): The input string to be validated. + param_spec (dict): The parameter's specification, including validation constraints. + param_name (str): The name of the parameter being validated. + invalid_params (list): A list to collect validation error messages. + + Returns: + str: The validated and possibly normalized string. + + Example `param_spec`: + { + "type": "str", + "length_max": 255 # Optional: maximum allowed length + } + """ + + item = validation.check_type_str(item) + if param_spec.get("length_max"): + if 1 <= len(item) <= param_spec.get("length_max"): + return item + else: + invalid_params.append( + "{0}:{1} : The string exceeds the allowed " + "range of max {2} char".format(param_name, item, param_spec.get("length_max")) + ) + return item + + +def validate_int(item, param_spec, param_name, invalid_params): + """ + This function checks that the input `item` is a valid integer and conforms to + the constraints specified in `param_spec`. If the integer is not valid or does + not meet the constraints, an error message is added to `invalid_params`. + + Args: + item (int): The input integer to be validated. + param_spec (dict): The parameter's specification, including validation constraints. + param_name (str): The name of the parameter being validated. + invalid_params (list): A list to collect validation error messages. + + Returns: + int: The validated integer. + + Example `param_spec`: + { + "type": "int", + "range_min": 1, # Optional: minimum allowed value + "range_max": 100 # Optional: maximum allowed value + } + """ + + item = validation.check_type_int(item) + min_value = 1 + if param_spec.get("range_min") is not None: + min_value = param_spec.get("range_min") + if param_spec.get("range_max"): + if min_value <= item <= param_spec.get("range_max"): + return item + else: + invalid_params.append( + "{0}:{1} : The item exceeds the allowed " + "range of max {2}".format(param_name, item, param_spec.get("range_max")) + ) + return item + + +def validate_bool(item, param_spec, param_name, invalid_params): + """ + This function checks that the input `item` is a valid boolean value. If it does + not represent a valid boolean value, an error message is added to `invalid_params`. + + Args: + item (bool): The input boolean value to be validated. + param_spec (dict): The parameter's specification, including validation constraints. + param_name (str): The name of the parameter being validated. + invalid_params (list): A list to collect validation error messages. + + Returns: + bool: The validated boolean value. + """ + + return validation.check_type_bool(item) + + +def validate_list(item, param_spec, param_name, invalid_params): + """ + This function checks if the input `item` is a valid list based on the specified `param_spec`. + It also verifies that the elements of the list match the expected data type specified in the + `param_spec`. If any validation errors occur, they are appended to the `invalid_params` list. + + Args: + item (list): The input list to be validated. + param_spec (dict): The parameter's specification, including validation constraints. + param_name (str): The name of the parameter being validated. + invalid_params (list): A list to collect validation error messages. + + Returns: + list: The validated list, potentially normalized based on the specification. + """ + + try: + if param_spec.get("type") == type(item).__name__: + keys_list = [] + for dict_key in param_spec: + keys_list.append(dict_key) + if len(keys_list) == 1: + return validation.check_type_list(item) + + temp_dict = {keys_list[1]: param_spec[keys_list[1]]} + try: + if param_spec['elements']: + get_spec_type = param_spec['type'] + get_spec_element = param_spec['elements'] + if type(item).__name__ == get_spec_type: + for element in item: + if type(element).__name__ != get_spec_element: + invalid_params.append( + "{0} is not of the same datatype as expected which is {1}".format(element, get_spec_element) + ) + else: + invalid_params.append( + "{0} is not of the same datatype as expected which is {1}".format(item, get_spec_type) + ) + except Exception as e: + item, list_invalid_params = validate_list_of_dicts(item, temp_dict) + invalid_params.extend(list_invalid_params) + else: + invalid_params.append("{0} : is not a valid list".format(item)) + except Exception as e: + invalid_params.append("{0} : comes into the exception".format(e)) + + return item + + +def validate_dict(item, param_spec, param_name, invalid_params): + """ + This function checks if the input `item` is a valid dictionary based on the specified `param_spec`. + If the dictionary does not match the expected data type specified in the `param_spec`, + a validation error is appended to the `invalid_params` list. + + Args: + item (dict): The input dictionary to be validated. + param_spec (dict): The parameter's specification, including validation constraints. + param_name (str): The name of the parameter being validated. + invalid_params (list): A list to collect validation error messages. + + Returns: + dict: The validated dictionary. + """ + + if param_spec.get("type") != type(item).__name__: + invalid_params.append("{0} : is not a valid dictionary".format(item)) + return validation.check_type_dict(item) + + def validate_list_of_dicts(param_list, spec, module=None): """Validate/Normalize playbook params. Will raise when invalid parameters found. param_list: a playbook parameter list of dicts @@ -211,11 +480,17 @@ def validate_list_of_dicts(param_list, spec, module=None): foo=dict(type='str', default='bar')) return: list of normalized input data """ + v = validation normalized = [] invalid_params = [] + for list_entry in param_list: valid_params_dict = {} + if not spec: + # Handle the case when spec becomes empty but param list is still there + invalid_params.append("No more spec to validate, but parameters remain") + break for param in spec: item = list_entry.get(param) log(str(item)) @@ -226,58 +501,41 @@ def validate_list_of_dicts(param_list, spec, module=None): ) else: item = spec[param].get("default") + valid_params_dict[param] = item + continue + data_type = spec[param].get("type") + switch = { + "str": validate_str, + "int": validate_int, + "bool": validate_bool, + "list": validate_list, + "dict": validate_dict, + } + + validator = switch.get(data_type) + if validator: + item = validator(item, spec[param], param, invalid_params) else: - type = spec[param].get("type") - if type == "str": - item = v.check_type_str(item) - if spec[param].get("length_max"): - if 1 <= len(item) <= spec[param].get("length_max"): - pass - else: - invalid_params.append( - "{0}:{1} : The string exceeds the allowed " - "range of max {2} char".format( - param, item, spec[param].get("length_max") - ) - ) - elif type == "int": - item = v.check_type_int(item) - min_value = 1 - if spec[param].get("range_min") is not None: - min_value = spec[param].get("range_min") - if spec[param].get("range_max"): - if min_value <= item <= spec[param].get("range_max"): - pass - else: - invalid_params.append( - "{0}:{1} : The item exceeds the allowed " - "range of max {2}".format( - param, item, spec[param].get("range_max") - ) - ) - elif type == "bool": - item = v.check_type_bool(item) - elif type == "list": - item = v.check_type_list(item) - elif type == "dict": - item = v.check_type_dict(item) - - choice = spec[param].get("choices") - if choice: - if item not in choice: - invalid_params.append( - "{0} : Invalid choice provided".format(item) - ) + invalid_params.append( + "{0}:{1} : Unsupported data type {2}.".format(param, item, data_type) + ) - no_log = spec[param].get("no_log") - if no_log: - if module is not None: - module.no_log_values.add(item) - else: - msg = "\n\n'{0}' is a no_log parameter".format(param) - msg += "\nAnsible module object must be passed to this " - msg += "\nfunction to ensure it is not logged\n\n" - raise Exception(msg) + choice = spec[param].get("choices") + if choice: + if item not in choice: + invalid_params.append( + "{0} : Invalid choice provided".format(item) + ) + + no_log = spec[param].get("no_log") + if no_log: + if module is not None: + module.no_log_values.add(item) + else: + msg = "\n\n'{0}' is a no_log parameter".format(param) + msg += "\nAnsible module object must be passed to this " + msg += "\nfunction to ensure it is not logged\n\n" + raise Exception(msg) valid_params_dict[param] = item normalized.append(valid_params_dict) diff --git a/plugins/modules/network_settings_intent.py b/plugins/modules/network_settings_intent.py new file mode 100644 index 0000000000..b00852247e --- /dev/null +++ b/plugins/modules/network_settings_intent.py @@ -0,0 +1,1957 @@ +#!/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 global pool, reserve pool and network in DNAC.""" +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = ['Muthu Rakesh, Madhan Sankaranarayanan'] + +DOCUMENTATION = r""" +--- +module: network_settings_intent +short_description: Resource module for IP Address pools and network functions +description: +- Manage operations on Global Pool, Reserve Pool, Network resources. +- API to create/update/delete global pool. +- API to reserve/update/delete an ip subpool from the global pool. +- API to update network settings for DHCP, Syslog, SNMP, NTP, Network AAA, Client and Endpoint AAA, + and/or DNS center server settings. +version_added: '6.6.0' +extends_documentation_fragment: + - cisco.dnac.intent_params +author: Muthu Rakesh (@MUTHU-RAKESH-27) + Madhan Sankaranarayanan (@madhansansel) +options: + state: + description: The state of DNAC after module completion. + type: str + choices: [ merged, deleted ] + default: merged + config: + description: + - List of details of global pool, reserved pool, network being managed. + type: list + elements: dict + required: true + suboptions: + GlobalPoolDetails: + description: Global ip pool manages IPv4 and IPv6 IP pools. + type: dict + suboptions: + settings: + description: Global Pool's settings. + type: dict + suboptions: + ippool: + description: Global Pool's ippool. + elements: dict + type: list + suboptions: + dhcpServerIps: + description: Dhcp Server Ips. + elements: str + type: list + dnsServerIps: + description: Dns Server Ips. + elements: str + type: list + gateway: + description: Gateway. + type: str + IpAddressSpace: + description: Ip address space. + type: str + ipPoolCidr: + description: Ip pool cidr. + type: str + prev_name: + description: previous name. + type: str + ipPoolName: + description: Ip Pool Name. + type: str + ReservePoolDetails: + description: Reserving IP subpool from the global pool + type: dict + suboptions: + ipv4DhcpServers: + description: IPv4 input for dhcp server ip example 1.1.1.1. + elements: str + type: list + ipv4DnsServers: + description: IPv4 input for dns server ip example 4.4.4.4. + elements: str + type: list + ipv4GateWay: + description: Gateway ip address details, example 175.175.0.1. + type: str + version_added: 4.0.0 + ipv4GlobalPool: + description: IP v4 Global pool address with cidr, example 175.175.0.0/16. + type: str + ipv4Prefix: + description: ip4 prefix length is enabled or ipv4 total Host input is enabled + type: bool + ipv4PrefixLength: + description: The ipv4 prefix length is required when ipv4prefix value is true. + type: int + ipv4Subnet: + description: IPv4 Subnet address, example 175.175.0.0. + type: str + ipv4TotalHost: + description: IPv4 total host is required when ipv4prefix value is false. + type: int + ipv6AddressSpace: + description: > + If the value is false only ipv4 input are required, otherwise both + ipv6 and ipv4 are required. + type: bool + ipv6DhcpServers: + description: IPv6 format dhcp server as input example 2001 db8 1234. + elements: str + type: list + ipv6DnsServers: + description: IPv6 format dns server input example 2001 db8 1234. + elements: str + type: list + ipv6GateWay: + description: Gateway ip address details, example 2001 db8 85a3 0 100 1. + type: str + ipv6GlobalPool: + description: > + IPv6 Global pool address with cidr this is required when Ipv6AddressSpace + value is true, example 2001 db8 85a3 /64. + type: str + ipv6Prefix: + description: > + Ipv6 prefix value is true, the ip6 prefix length input field is enabled, + if it is false ipv6 total Host input is enable. + type: bool + ipv6PrefixLength: + description: IPv6 prefix length is required when the ipv6prefix value is true. + type: int + ipv6Subnet: + description: IPv6 Subnet address, example 2001 db8 85a3 0 100. + type: str + ipv6TotalHost: + description: IPv6 total host is required when ipv6prefix value is false. + type: int + name: + description: Name of the reserve ip sub pool. + type: str + prev_name: + description: Previous name of the reserve ip sub pool. + type: str + siteName: + description: Site name path parameter. Site name to reserve the ip sub pool. + type: str + slaacSupport: + description: Slaac Support. + type: bool + type: + description: Type of the reserve ip sub pool. + type: str + NetworkManagementDetails: + description: Set default network settings for the site + type: dict + suboptions: + settings: + description: Network management details settings. + type: dict + suboptions: + clientAndEndpoint_aaa: + description: Network V2's clientAndEndpoint_aaa. + suboptions: + ipAddress: + description: IP address for ISE serve (eg 1.1.1.4). + type: str + network: + description: IP address for AAA or ISE server (eg 2.2.2.1). + type: str + protocol: + description: Protocol for AAA or ISE serve (eg RADIUS). + type: str + servers: + description: Server type AAA or ISE server (eg AAA). + type: str + sharedSecret: + description: Shared secret for ISE server. + type: str + type: dict + dhcpServer: + description: DHCP Server IP (eg 1.1.1.1). + elements: str + type: list + dnsServer: + description: Network V2's dnsServer. + suboptions: + domainName: + description: Domain Name of DHCP (eg; cisco). + type: str + primaryIpAddress: + description: Primary IP Address for DHCP (eg 2.2.2.2). + type: str + secondaryIpAddress: + description: Secondary IP Address for DHCP (eg 3.3.3.3). + type: str + type: dict + messageOfTheday: + description: Network V2's messageOfTheday. + suboptions: + bannerMessage: + description: Massage for Banner message (eg; Good day). + type: str + retainExistingBanner: + description: Retain existing Banner Message (eg "true" or "false"). + type: str + type: dict + netflowcollector: + description: Network V2's netflowcollector. + suboptions: + ipAddress: + description: IP Address for NetFlow collector (eg 3.3.3.1). + type: str + port: + description: Port for NetFlow Collector (eg; 443). + type: int + type: dict + network_aaa: + description: Network V2's network_aaa. + suboptions: + ipAddress: + description: IP address for AAA and ISE server (eg 1.1.1.1). + type: str + network: + description: IP Address for AAA or ISE server (eg 2.2.2.2). + type: str + protocol: + description: Protocol for AAA or ISE serve (eg RADIUS). + type: str + servers: + description: Server type for AAA Network (eg AAA). + type: str + sharedSecret: + description: Shared secret for ISE Server. + type: str + type: dict + ntpServer: + description: IP address for NTP server (eg 1.1.1.2). + elements: str + type: list + snmpServer: + description: Network V2's snmpServer. + suboptions: + configureDnacIP: + description: Configuration DNAC IP for SNMP Server (eg true). + type: bool + ipAddresses: + description: IP Address for SNMP Server (eg 4.4.4.1). + elements: str + type: list + type: dict + syslogServer: + description: Network V2's syslogServer. + suboptions: + configureDnacIP: + description: Configuration DNAC IP for syslog server (eg true). + type: bool + ipAddresses: + description: IP Address for syslog server (eg 4.4.4.4). + elements: str + type: list + type: dict + timezone: + description: Input for time zone (eg Africa/Abidjan). + type: str + siteName: + description: Site name path parameter. + type: str +requirements: +- dnacentersdk == 2.4.5 +- python >= 3.5 +notes: + - SDK Method used are + network_settings.NetworkSettings.create_global_pool, + network_settings.NetworkSettings.delete_global_ip_pool, + network_settings.NetworkSettings.update_global_pool, + network_settings.NetworkSettings.release_reserve_ip_subpool, + network_settings.NetworkSettings.reserve_ip_subpool, + network_settings.NetworkSettings.update_reserve_ip_subpool, + network_settings.NetworkSettings.update_network_v2, + + - Paths used are + post /dna/intent/api/v1/global-pool, + delete /dna/intent/api/v1/global-pool/{id}, + put /dna/intent/api/v1/global-pool, + post /dna/intent/api/v1/reserve-ip-subpool/{siteId}, + delete /dna/intent/api/v1/reserve-ip-subpool/{id}, + put /dna/intent/api/v1/reserve-ip-subpool/{siteId}, + put /dna/intent/api/v2/network/{siteId}, + +""" + +EXAMPLES = r""" +- name: Create global pool, reserve an ip pool and network + cisco.dnac.network_settings_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: + - GlobalPoolDetails: + settings: + ippool: + - ipPoolName: string + gateway: string + IpAddressSpace: string + ipPoolCidr: string + type: Generic + dhcpServerIps: list + dnsServerIps: list + ReservePoolDetails: + ipv6AddressSpace: True + ipv4GlobalPool: string + ipv4Prefix: True + ipv4PrefixLength: 9 + ipv4Subnet: string + name: string + ipv6Prefix: True + ipv6PrefixLength: 64 + ipv6GlobalPool: string + ipv6Subnet: string + siteName: string + slaacSupport: True + type: LAN + NetworkManagementDetails: + settings: + dhcpServer: list + dnsServer: + domainName: string + primaryIpAddress: string + secondaryIpAddress: string + clientAndEndpoint_aaa: + network: string + protocol: string + servers: string + messageOfTheday: + bannerMessage: string + retainExistingBanner: string + netflowcollector: + ipAddress: string + port: 443 + network_aaa: + network: string + protocol: string + servers: string + ntpServer: list + snmpServer: + configureDnacIP: True + ipAddresses: list + syslogServer: + configureDnacIP: True + ipAddresses: list + siteName: string +""" + +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 + returned: always + type: dict + sample: > + { + "executionId": "string", + "executionStatusUrl": "string", + "message": "string" + } + +# 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 + returned: always + type: dict + sample: > + { + "executionId": "string", + "executionStatusUrl": "string", + "message": "string" + } + +# Case_3: Successful creation/updation of network +response_3: + description: A dictionary or list with the response returned by the Cisco DNAC Python SDK + returned: always + type: dict + sample: > + { + "executionId": "string", + "executionStatusUrl": "string", + "message": "string" + } +""" + +import copy +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, + dnac_compare_equality, +) + + +class DnacNetwork(DnacBase): + """Class containing member attributes for network intent module""" + + def __init__(self, module): + super().__init__(module) + self.result["response"] = [ + {"globalPool": {"response": {}, "msg": {}}}, + {"reservePool": {"response": {}, "msg": {}}}, + {"network": {"response": {}, "msg": {}}} + ] + + 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 + + Returns: + self + + """ + + 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 = { + "GlobalPoolDetails": { + "type": 'dict', + "settings": { + "type": 'dict', + "ippool": { + "type": 'list', + "IpAddressSpace": {"type": 'string'}, + "dhcpServerIps": {"type": 'list'}, + "dnsServerIps": {"type": 'list'}, + "gateway": {"type": 'string'}, + "ipPoolCidr": {"type": 'string'}, + "ipPoolName": {"type": 'string'}, + "prevName": {"type": 'string'}, + } + } + }, + "ReservePoolDetails": { + "type": 'dict', + "name": {"type": 'string'}, + "prevName": {"type": 'string'}, + "ipv6AddressSpace": {"type": 'bool'}, + "ipv4GlobalPool": {"type": 'string'}, + "ipv4Prefix": {"type": 'bool'}, + "ipv4PrefixLength": {"type": 'string'}, + "ipv4Subnet": {"type": 'string'}, + "ipv4GateWay": {"type": 'string'}, + "ipv4DhcpServers": {"type": 'list'}, + "ipv4DnsServers": {"type": 'list'}, + "ipv6GlobalPool": {"type": 'string'}, + "ipv6Prefix": {"type": 'bool'}, + "ipv6PrefixLength": {"type": 'integer'}, + "ipv6Subnet": {"type": 'string'}, + "ipv6GateWay": {"type": 'string'}, + "ipv6DhcpServers": {"type": 'list'}, + "ipv6DnsServers": {"type": 'list'}, + "ipv4TotalHost": {"type": 'integer'}, + "ipv6TotalHost": {"type": 'integer'}, + "slaacSupport": {"type": 'bool'}, + "siteName": {"type": 'string'}, + }, + "NetworkManagementDetails": { + "type": 'dict', + "settings": { + "type": 'dict', + "dhcpServer": {"type": 'list'}, + "dnsServer": { + "type": 'dict', + "domainName": {"type": 'string'}, + "primaryIpAddress": {"type": 'string'}, + "secondaryIpAddress": {"type": 'string'} + }, + "syslogServer": { + "type": 'dict', + "ipAddresses": {"type": 'list'}, + "configureDnacIP": {"type": 'bool'} + }, + "snmpServer": { + "type": 'dict', + "ipAddresses": {"type": 'list'}, + "configureDnacIP": {"type": 'bool'} + }, + "netflowcollector": { + "type": 'dict', + "ipAddress": {"type": 'string'}, + "port": {"type": 'integer'}, + }, + "timezone": {"type": 'string'}, + "ntpServer": {"type": 'list'}, + "messageOfTheday": { + "type": 'dict', + "bannerMessage": {"type": 'string'}, + "retainExistingBanner": {"type": 'bool'}, + }, + "network_aaa": { + "type": 'dict', + "servers": {"type": 'string', "choices": ["ISE", "AAA"]}, + "ipAddress": {"type": 'string'}, + "network": {"type": 'string'}, + "protocol": {"type": 'string', "choices": ["RADIUS", "TACACS"]}, + "sharedSecret": {"type": 'string'} + + }, + "clientAndEndpoint_aaa": { + "type": 'dict', + "servers": {"type": 'string', "choices": ["ISE", "AAA"]}, + "ipAddress": {"type": 'string'}, + "network": {"type": 'string'}, + "protocol": {"type": 'string', "choices": ["RADIUS", "TACACS"]}, + "sharedSecret": {"type": 'string'} + } + }, + "siteName": {"type": 'string'}, + } + } + + # 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 requires_update(self, have, want, obj_params): + """ + Check if the template config given requires update by comparing + 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 + from the playbook, using a specified schema for comparison. + + Parameters: + have (dict) - Current information from the DNAC + (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). + + 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. + + """ + + current_obj = have + requested_obj = want + self.log(str(current_obj)) + self.log(str(requested_obj)) + + return any(not dnac_compare_equality(current_obj.get(dnac_param), + requested_obj.get(ansible_param)) + for (dnac_param, ansible_param) in 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 + + 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(str(_id)) + except Exception as e: + self.log("Error while getting site_id from the site_name") + return None + + return _id + + def get_global_pool_params(self, pool_info): + """ + Process Global Pool params from playbook data for Global Pool config in DNAC + + Parameters: + pool_info (dict) - Playbook data containing information about the global pool + + Returns: + dict or None - Processed Global Pool data in a format suitable + for DNAC configuration, or None if pool_info is empty. + """ + + if not pool_info: + self.log("Global Pool is empty") + return None + + self.log(str(pool_info)) + global_pool = { + "settings": { + "ippool": [{ + "dhcpServerIps": pool_info.get("dhcpServerIps"), + "dnsServerIps": pool_info.get("dnsServerIps"), + "ipPoolCidr": pool_info.get("ipPoolCidr"), + "ipPoolName": pool_info.get("ipPoolName"), + "type": pool_info.get("type") + }] + } + } + self.log(str(global_pool)) + global_ippool = global_pool.get("settings").get("ippool")[0] + if pool_info.get("ipv6") is False: + global_ippool.update({"IpAddressSpace": "IPv4"}) + else: + global_ippool.update({"IpAddressSpace": "IPv6"}) + + self.log(str(global_ippool.get("IpAddressSpace"))) + if not pool_info["gateways"]: + global_ippool.update({"gateway": ""}) + else: + global_ippool.update({"gateway": pool_info.get("gateways")[0]}) + + return global_pool + + def get_reserve_pool_params(self, pool_info): + """ + Process Reserved Pool parameters from playbook data for Reserved Pool configuration in DNAC + + Parameters: + pool_info (dict) - Playbook data containing information about the reserved pool + + Returns: + reserve_pool (dict) - Processed Reserved pool data + in the format suitable for the DNAC config + """ + + reserve_pool = { + "name": pool_info.get("groupName"), + "site_id": pool_info.get("siteId"), + } + if len(pool_info.get("ipPools")) == 1: + reserve_pool.update({ + "ipv4DhcpServers": pool_info.get("ipPools")[0].get("dhcpServerIps"), + "ipv4DnsServers": pool_info.get("ipPools")[0].get("dnsServerIps"), + "ipv6AddressSpace": "False" + }) + if pool_info.get("ipPools")[0].get("gateways") != []: + reserve_pool.update({"ipv4GateWay": pool_info.get("ipPools")[0].get("gateways")[0]}) + else: + reserve_pool.update({"ipv4GateWay": ""}) + reserve_pool.update({"ipv6AddressSpace": "False"}) + elif len(pool_info.get("ipPools")) == 2: + if not pool_info.get("ipPools")[0].get("ipv6"): + reserve_pool.update({ + "ipv4DhcpServers": pool_info.get("ipPools")[0].get("dhcpServerIps"), + "ipv4DnsServers": pool_info.get("ipPools")[0].get("dnsServerIps"), + "ipv6AddressSpace": "True", + "ipv6DhcpServers": pool_info.get("ipPools")[1].get("dhcpServerIps"), + "ipv6DnsServers": pool_info.get("ipPools")[1].get("dnsServerIps"), + + }) + + if pool_info.get("ipPools")[0].get("gateways") != []: + reserve_pool.update({"ipv4GateWay": + pool_info.get("ipPools")[0].get("gateways")[0]}) + else: + reserve_pool.update({"ipv4GateWay": ""}) + + if pool_info.get("ipPools")[1].get("gateways") != []: + reserve_pool.update({"ipv6GateWay": + pool_info.get("ipPools")[1].get("gateways")[0]}) + else: + reserve_pool.update({"ipv4GateWay": ""}) + + elif not pool_info.get("ipPools")[1].get("ipv6"): + reserve_pool.update({ + "ipv4DhcpServers": pool_info.get("ipPools")[1].get("dhcpServerIps"), + "ipv4DnsServers": pool_info.get("ipPools")[1].get("dnsServerIps"), + "ipv6AddressSpace": "True", + "ipv6DnsServers": pool_info.get("ipPools")[0].get("dnsServerIps"), + "ipv6DhcpServers": pool_info.get("ipPools")[0].get("dhcpServerIps") + }) + if pool_info.get("ipPools")[1].get("gateways") != []: + reserve_pool.update({"ipv4GateWay": + pool_info.get("ipPools")[1].get("gateways")[0]}) + else: + reserve_pool.update({"ipv4GateWay": ""}) + + if pool_info.get("ipPools")[0].get("gateways") != []: + reserve_pool.update({"ipv6GateWay": + pool_info.get("ipPools")[0].get("gateways")[0]}) + else: + reserve_pool.update({"ipv4GateWay": ""}) + reserve_pool.update({"slaacSupport": True}) + self.log(str(reserve_pool)) + return reserve_pool + + def get_network_params(self, site_id): + """ + Process the Network parameters from the playbook for Network configuration in DNAC + + Parameters: + site_id (str) - The Site ID for which network parameters are requested + + Returns: + dict or None: Processed Network data in a format suitable for DNAC configuration, + or None if the response is not a dictionary or there was an error. + """ + + response = self.dnac._exec( + family="network_settings", + function='get_network', + params={"site_id": site_id} + ) + self.log(str(response)) + if not isinstance(response, dict): + self.log("Error in getting network details - Response is not a dictionary") + return None + + # Extract various network-related details from the response + all_network_details = response.get("response") + dhcp_details = get_dict_result(all_network_details, "key", "dhcp.server") + dns_details = get_dict_result(all_network_details, "key", "dns.server") + snmp_details = get_dict_result(all_network_details, "key", "snmp.trap.receiver") + syslog_details = get_dict_result(all_network_details, "key", "syslog.server") + netflow_details = get_dict_result(all_network_details, "key", "netflow.collector") + ntpserver_details = get_dict_result(all_network_details, "key", "ntp.server") + timezone_details = get_dict_result(all_network_details, "key", "timezone.site") + messageoftheday_details = get_dict_result(all_network_details, "key", "banner.setting") + network_aaa = get_dict_result(all_network_details, "key", "aaa.network.server.1") + network_aaa_pan = get_dict_result(all_network_details, "key", "aaa.server.pan.network") + clientAndEndpoint_aaa = get_dict_result(all_network_details, "key", "aaa.endpoint.server.1") + clientAndEndpoint_aaa_pan = \ + get_dict_result(all_network_details, "key", "aaa.server.pan.endpoint") + + # Prepare the network details for DNAC configuration + network_details = { + "settings": { + "snmpServer": { + "configureDnacIP": snmp_details.get("value")[0].get("configureDnacIP"), + "ipAddresses": snmp_details.get("value")[0].get("ipAddresses"), + }, + "syslogServer": { + "configureDnacIP": syslog_details.get("value")[0].get("configureDnacIP"), + "ipAddresses": syslog_details.get("value")[0].get("ipAddresses"), + }, + "netflowcollector": { + "ipAddress": netflow_details.get("value")[0].get("ipAddress"), + "port": netflow_details.get("value")[0].get("port"), + "configureDnacIP": netflow_details.get("value")[0].get("configureDnacIP"), + }, + "timezone": timezone_details.get("value")[0], + } + } + network_settings = network_details.get("settings") + if dhcp_details is not None: + network_settings.update({"dhcpServer": dhcp_details.get("value")}) + + if dns_details is not None: + network_settings.update({ + "dnsServer": { + "domainName": dns_details.get("value")[0].get("domainName"), + "primaryIpAddress": dns_details.get("value")[0].get("primaryIpAddress"), + "secondaryIpAddress": dns_details.get("value")[0].get("secondaryIpAddress") + } + }) + + if ntpserver_details is not None: + network_settings.update({"ntpServer": ntpserver_details.get("value")}) + + if messageoftheday_details is not None: + network_settings.update({ + "messageOfTheday": { + "bannerMessage": messageoftheday_details.get("value")[0].get("bannerMessage"), + "retainExistingBanner": + messageoftheday_details.get("value")[0].get("retainExistingBanner"), + } + }) + + if network_aaa and network_aaa_pan: + network_settings.update({ + "network_aaa": { + "network": network_aaa.get("value")[0].get("ipAddress"), + "protocol": network_aaa.get("value")[0].get("protocol"), + "ipAddress": network_aaa_pan.get("value")[0] + } + }) + + if clientAndEndpoint_aaa and clientAndEndpoint_aaa_pan: + network_settings.update({ + "clientAndEndpoint_aaa": { + "network": clientAndEndpoint_aaa.get("value")[0].get("ipAddress"), + "protocol": clientAndEndpoint_aaa.get("value")[0].get("protocol"), + "ipAddress": clientAndEndpoint_aaa_pan.get("value")[0], + } + }) + self.log(str(network_details)) + return network_details + + 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 + + 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. + """ + + global_pool = { + "exists": False, + "details": None, + "id": None + } + response = self.dnac._exec( + family="network_settings", + function="get_global_pool", + ) + if not isinstance(response, dict): + self.log("Error in getting global pool - Response is not a dictionary") + return global_pool + + all_global_pool_details = response.get("response") + global_pool_details = get_dict_result(all_global_pool_details, "ipPoolName", name) + self.log("Global Ippool Name : " + str(name)) + self.log(str(global_pool_details)) + if not global_pool_details: + self.log("Global pool {0} does not exist".format(name)) + return global_pool + global_pool.update({"exists": True}) + global_pool.update({"id": global_pool_details.get("id")}) + global_pool["details"] = self.get_global_pool_params(global_pool_details) + + self.log(str(global_pool)) + return global_pool + + def reserve_pool_exists(self, name, site_name): + """ + Check if the Reserved pool with the given name exists in a specific site + 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. + + 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. + """ + + reserve_pool = { + "exists": False, + "details": None, + "id": None, + "success": True + } + site_id = self.get_site_id(site_name) + self.log(str(site_id)) + if not site_id: + reserve_pool.update({"success": False}) + self.msg = "Failed to get the site id from the site name {0}".format(site_name) + self.status = "failed" + return reserve_pool + + response = self.dnac._exec( + family="network_settings", + function="get_reserve_ip_subpool", + params={"siteId": site_id} + ) + if not isinstance(response, dict): + reserve_pool.update({"success": False}) + self.msg = "Error in getting reserve pool - Response is not a dictionary" + self.status = "exited" + return reserve_pool + + all_reserve_pool_details = response.get("response") + reserve_pool_details = get_dict_result(all_reserve_pool_details, "groupName", name) + if not reserve_pool_details: + self.log("Reserve pool {0} does not exist in the site {1}".format(name, site_name)) + return reserve_pool + + reserve_pool.update({"exists": True}) + reserve_pool.update({"id": reserve_pool_details.get("id")}) + reserve_pool.update({"details": self.get_reserve_pool_params(reserve_pool_details)}) + + self.log("Reserved Pool Details " + str(reserve_pool.get("details"))) + self.log("Reserved Pool Id " + str(reserve_pool.get("id"))) + return reserve_pool + + def get_have_global_pool(self, config): + """ + Get the current Global Pool information from + DNAC based on the provided playbook details. + check this API using check_return_status. + + Parameters: + config (dict) - Playbook details containing Global Pool configuration. + + Returns: + self - The current object with updated information. + """ + + global_pool = { + "exists": False, + "details": None, + "id": None + } + global_pool_settings = config.get("GlobalPoolDetails").get("settings") + if global_pool_settings is None: + self.msg = "settings in GlobalPoolDetails is missing in the playbook" + self.status = "failed" + return self + + global_pool_ippool = global_pool_settings.get("ippool") + if global_pool_ippool is None: + self.msg = "ippool in GlobalPoolDetails is missing in the playbook" + self.status = "failed" + return self + + name = global_pool_ippool[0].get("ipPoolName") + if name is None: + self.msg = "Mandatory Parameter ipPoolName required" + self.status = "failed" + return self + + # If the Global Pool doesn't exist and a previous name is provided + # Else try using the previous name + global_pool = self.global_pool_exists(name) + self.log(str(global_pool)) + prev_name = global_pool_ippool[0].get("prev_name") + if global_pool.get("exists") is False and \ + prev_name is not None: + global_pool = self.global_pool_exists(prev_name) + if global_pool.get("exists") is False: + self.msg = "Prev name {0} doesn't exist in GlobalPoolDetails".format(prev_name) + self.status = "failed" + return self + + 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.status = "success" + return self + + def get_have_reserve_pool(self, config): + """ + Get the current Reserved Pool information from DNAC + based on the provided playbook details. + Check this API using check_return_status + + Parameters: + config (list of dict) - Playbook details containing Reserved Pool configuration. + + Returns: + self - The current object with updated information. + """ + + reserve_pool = { + "exists": False, + "details": None, + "id": None + } + reserve_pool_details = config.get("ReservePoolDetails") + name = reserve_pool_details.get("name") + if name is None: + self.msg = "Mandatory Parameter name required in ReservePoolDetails\n" + self.status = "failed" + return self + + site_name = reserve_pool_details.get("siteName") + self.log(str(site_name)) + if site_name is None: + self.msg = "Missing parameter 'siteName' in ReservePoolDetails" + self.status = "failed" + return self + + # Check if the Reserved Pool exists in DNAC 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() + self.log(str(reserve_pool)) + + # If the Reserved Pool doesn't exist and a previous name is provided + # Else try using the previous name + prev_name = reserve_pool_details.get("prev_name") + if reserve_pool.get("exists") is False and \ + prev_name is not None: + reserve_pool = self.reserve_pool_exists(prev_name, site_name) + if not reserve_pool.get("success"): + return self.check_return_status() + + # If the previous name doesn't exist in DNAC, 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" + return self + + self.log("Reservation Exists: " + str(reserve_pool.get("exists")) + + "\n Reserved Pool: " + str(reserve_pool.get("details"))) + + # If reserve pool exist, convert ipv6AddressSpace to the required format (boolean) + if reserve_pool.get("exists"): + reserve_pool_details = reserve_pool.get("details") + if reserve_pool_details.get("ipv6AddressSpace") == "False": + reserve_pool_details.update({"ipv6AddressSpace": False}) + else: + reserve_pool_details.update({"ipv6AddressSpace": True}) + + self.log(str(reserve_pool)) + self.have.update({"reservePool": reserve_pool}) + self.msg = "Collecting the reserve pool details from the DNAC" + self.status = "success" + return self + + def get_have_network(self, config): + """ + Get the current Network details from DNAC based on the provided playbook details. + + Parameters: + config (dict) - Playbook details containing Network Management configuration. + + Returns: + self - The current object with updated Network information. + """ + network = {} + site_name = config.get("NetworkManagementDetails").get("siteName") + if site_name is None: + self.msg = "Mandatory Parameter 'siteName' missing" + self.status = "failed" + return self + + site_id = self.get_site_id(site_name) + if site_id is None: + self.msg = "Failed to get site id from {0}".format(site_name) + self.status = "failed" + return self + + network["site_id"] = site_id + network["net_details"] = self.get_network_params(site_id) + self.log("Network Details from the DNAC " + str(network)) + self.have.update({"network": network}) + self.msg = "Collecting the network details from the DNAC" + self.status = "success" + return self + + def get_have(self, config): + """ + Get the current Global Pool Reserved Pool and Network details from DNAC + + Parameters: + 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. + """ + + if config.get("GlobalPoolDetails") is not None: + self.get_have_global_pool(config).check_return_status() + + if config.get("ReservePoolDetails") is not None: + self.get_have_reserve_pool(config).check_return_status() + + 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.status = "success" + return self + + def get_want_global_pool(self, global_ippool): + """ + Get all the Global Pool information from playbook + Set the status and the msg before returning from the API + 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. + + Returns: + self - The current object with updated desired Global Pool information. + """ + + # Initialize the desired Global Pool configuration + want_global = { + "settings": { + "ippool": [{ + "IpAddressSpace": global_ippool.get("IpAddressSpace"), + "dhcpServerIps": global_ippool.get("dhcpServerIps"), + "dnsServerIps": global_ippool.get("dnsServerIps"), + "ipPoolName": global_ippool.get("ipPoolName"), + "ipPoolCidr": global_ippool.get("ipPoolCidr"), + "gateway": global_ippool.get("gateway"), + "type": global_ippool.get("type"), + }] + } + } + want_ippool = want_global.get("settings").get("ippool")[0] + + # Converting to the required format based on the existing Global Pool + if not self.have.get("globalPool").get("exists"): + if want_ippool.get("dhcpServerIps") is None: + want_ippool.update({"dhcpServerIps": []}) + if want_ippool.get("dnsServerIps") is None: + want_ippool.update({"dnsServerIps": []}) + if want_ippool.get("IpAddressSpace") is None: + want_ippool.update({"IpAddressSpace": ""}) + if want_ippool.get("gateway") is None: + want_ippool.update({"gateway": ""}) + if want_ippool.get("type") is None: + want_ippool.update({"type": "Generic"}) + else: + have_ippool = self.have.get("globalPool").get("details") \ + .get("settings").get("ippool")[0] + + # Copy existing Global Pool information if the desired configuration is not provided + want_ippool.update({ + "IpAddressSpace": have_ippool.get("IpAddressSpace"), + "type": have_ippool.get("ipPoolType"), + "ipPoolCidr": have_ippool.get("ipPoolCidr") + }) + want_ippool.update({}) + want_ippool.update({}) + + for key in ["dhcpServerIps", "dnsServerIps", "gateway"]: + if want_ippool.get(key) is None and have_ippool.get(key) is not None: + want_ippool[key] = have_ippool[key] + + self.log("Global Pool Playbook Details " + str(want_global)) + self.want.update({"wantGlobal": want_global}) + self.msg = "Collecting the global pool details from the playbook" + self.status = "success" + return self + + def get_want_reserve_pool(self, reserve_pool): + """ + Get all the Reserved Pool information from playbook + Set the status and the msg before returning from the API + Check the return value of the API with check_return_status() + + Parameters: + reserve_pool (dict) - Playbook reserved pool + details containing various properties. + + Returns: + self - The current object with updated desired Reserved Pool information. + """ + + want_reserve = { + "name": reserve_pool.get("name"), + "type": reserve_pool.get("type"), + "ipv6AddressSpace": reserve_pool.get("ipv6AddressSpace"), + "ipv4GlobalPool": reserve_pool.get("ipv4GlobalPool"), + "ipv4Prefix": reserve_pool.get("ipv4Prefix"), + "ipv4PrefixLength": reserve_pool.get("ipv4PrefixLength"), + "ipv4GateWay": reserve_pool.get("ipv4GateWay"), + "ipv4DhcpServers": reserve_pool.get("ipv4DhcpServers"), + "ipv4DnsServers": reserve_pool.get("ipv4DnsServers"), + "ipv4Subnet": reserve_pool.get("ipv4Subnet"), + "ipv6GlobalPool": reserve_pool.get("ipv6GlobalPool"), + "ipv6Prefix": reserve_pool.get("ipv6Prefix"), + "ipv6PrefixLength": reserve_pool.get("ipv6PrefixLength"), + "ipv6GateWay": reserve_pool.get("ipv6GateWay"), + "ipv6DhcpServers": reserve_pool.get("ipv6DhcpServers"), + "ipv6Subnet": reserve_pool.get("ipv6Subnet"), + "ipv6DnsServers": reserve_pool.get("ipv6DnsServers"), + "ipv4TotalHost": reserve_pool.get("ipv4TotalHost"), + "ipv6TotalHost": reserve_pool.get("ipv6TotalHost") + } + + # Check for missing mandatory parameters in the playbook + if not want_reserve.get("name"): + self.msg = "Missing mandatory parameter 'name' in ReservePoolDetails" + self.status = "failed" + return self + + if want_reserve.get("ipv4Prefix") is True: + if want_reserve.get("ipv4Subnet") is None and \ + want_reserve.get("ipv4TotalHost") is None: + self.msg = "missing parameter 'ipv4Subnet' or 'ipv4TotalHost' \ + while adding the ipv4 in ReservePoolDetails" + self.status = "failed" + return self + + if want_reserve.get("ipv6Prefix") is True: + if want_reserve.get("ipv6Subnet") is None and \ + want_reserve.get("ipv6TotalHost") is None: + self.msg = "missing parameter 'ipv6Subnet' or 'ipv6TotalHost' \ + while adding the ipv6 in ReservePoolDetails" + self.status = "failed" + return self + + self.log("Reserve IP Pool Playbook Details " + str(want_reserve)) + + # If there are no existing Reserved Pool details, validate and set defaults + if not self.have.get("reservePool").get("details"): + if not want_reserve.get("ipv4GlobalPool"): + self.msg = "missing parameter 'ipv4GlobalPool' in ReservePoolDetails" + self.status = "failed" + return self + + if not want_reserve.get("ipv4PrefixLength"): + self.msg = "missing parameter 'ipv4PrefixLength' in ReservePoolDetails" + self.status = "failed" + return self + + if want_reserve.get("type") is None: + want_reserve.update({"type": "Generic"}) + if want_reserve.get("ipv4GateWay") is None: + want_reserve.update({"ipv4GateWay": ""}) + if want_reserve.get("ipv4DhcpServers") is None: + want_reserve.update({"ipv4DhcpServers": []}) + if want_reserve.get("ipv4DnsServers") is None: + want_reserve.update({"ipv4DnsServers": []}) + if want_reserve.get("ipv6AddressSpace") is None: + want_reserve.update({"ipv6AddressSpace": False}) + if want_reserve.get("slaacSupport") is None: + want_reserve.update({"slaacSupport": True}) + if want_reserve.get("ipv4TotalHost") is None: + del want_reserve['ipv4TotalHost'] + if want_reserve.get("ipv6AddressSpace") is True: + want_reserve.update({"ipv6Prefix": True}) + else: + del want_reserve['ipv6Prefix'] + + if not want_reserve.get("ipv6AddressSpace"): + keys_to_check = ['ipv6GlobalPool', 'ipv6PrefixLength', + 'ipv6GateWay', 'ipv6DhcpServers', + 'ipv6DnsServers', 'ipv6TotalHost'] + for key in keys_to_check: + if want_reserve.get(key) is None: + del want_reserve[key] + else: + keys_to_delete = ['type', 'ipv4GlobalPool', + 'ipv4Prefix', 'ipv4PrefixLength', + 'ipv4TotalHost', 'ipv4Subnet'] + for key in keys_to_delete: + if key in want_reserve: + del want_reserve[key] + + self.want.update({"wantReserve": want_reserve}) + self.log(str(self.want)) + self.msg = "Collecting the reserve pool details from the playbook" + self.status = "success" + return self + + def get_want_network(self, network_management_details): + """ + Get all the Network related information from playbook + Set the status and the msg before returning from the API + Check the return value of the API with check_return_status() + + Parameters: + network_management_details (dict) - Playbook network + details containing various network settings. + + Returns: + self - The current object with updated desired Network-related information. + """ + + want_network = { + "settings": { + "dhcpServer": {}, + "dnsServer": {}, + "snmpServer": {}, + "syslogServer": {}, + "netflowcollector": {}, + "ntpServer": {}, + "timezone": "", + "messageOfTheday": {}, + "network_aaa": {}, + "clientAndEndpoint_aaa": {} + } + } + want_network_settings = want_network.get("settings") + if network_management_details.get("dhcpServer"): + want_network_settings.update({ + "dhcpServer": network_management_details.get("dhcpServer") + }) + else: + del want_network_settings["dhcpServer"] + + if network_management_details.get("ntpServer"): + want_network_settings.update({ + "ntpServer": network_management_details.get("ntpServer") + }) + else: + del want_network_settings["ntpServer"] + + if network_management_details.get("timezone") is not None: + want_network_settings["timezone"] = \ + network_management_details.get("timezone") + else: + self.msg = "missing parameter timezone in network" + self.status = "failed" + return self + + dnsServer = network_management_details.get("dnsServer") + if dnsServer: + if dnsServer.get("domainName"): + want_network_settings.get("dnsServer").update({ + "domainName": + dnsServer.get("domainName") + }) + + if dnsServer.get("primaryIpAddress"): + want_network_settings.get("dnsServer").update({ + "primaryIpAddress": + dnsServer.get("primaryIpAddress") + }) + + if dnsServer.get("secondaryIpAddress"): + want_network_settings.get("dnsServer").update({ + "secondaryIpAddress": + dnsServer.get("secondaryIpAddress") + }) + else: + del want_network_settings["dnsServer"] + + snmpServer = network_management_details.get("snmpServer") + if snmpServer: + if snmpServer.get("configureDnacIP"): + want_network_settings.get("snmpServer").update({ + "configureDnacIP": snmpServer.get("configureDnacIP") + }) + if snmpServer.get("ipAddresses"): + want_network_settings.get("snmpServer").update({ + "ipAddresses": snmpServer.get("ipAddresses") + }) + else: + del want_network_settings["snmpServer"] + + syslogServer = network_management_details.get("syslogServer") + if syslogServer: + if syslogServer.get("configureDnacIP"): + want_network_settings.get("syslogServer").update({ + "configureDnacIP": syslogServer.get("configureDnacIP") + }) + if syslogServer.get("ipAddresses"): + want_network_settings.get("syslogServer").update({ + "ipAddresses": syslogServer.get("ipAddresses") + }) + else: + del want_network_settings["syslogServer"] + + netflowcollector = network_management_details.get("netflowcollector") + if netflowcollector: + if netflowcollector.get("ipAddress"): + want_network_settings.get("netflowcollector").update({ + "ipAddress": + netflowcollector.get("ipAddress") + }) + if netflowcollector.get("port"): + want_network_settings.get("netflowcollector").update({ + "port": + netflowcollector.get("port") + }) + if netflowcollector.get("configureDnacIP"): + want_network_settings.get("netflowcollector").update({ + "configureDnacIP": + netflowcollector.get("configureDnacIP") + }) + else: + del want_network_settings["netflowcollector"] + + messageOfTheday = network_management_details.get("messageOfTheday") + if messageOfTheday: + if messageOfTheday.get("bannerMessage"): + want_network_settings.get("messageOfTheday").update({ + "bannerMessage": + messageOfTheday.get("bannerMessage") + }) + if messageOfTheday.get("retainExistingBanner"): + want_network_settings.get("messageOfTheday").update({ + "retainExistingBanner": + messageOfTheday.get("retainExistingBanner") + }) + else: + del want_network_settings["messageOfTheday"] + + network_aaa = network_management_details.get("network_aaa") + if network_aaa: + if network_aaa.get("ipAddress"): + want_network_settings.get("network_aaa").update({ + "ipAddress": + network_aaa.get("ipAddress") + }) + else: + if network_aaa.get("servers") == "ISE": + self.msg = "missing parameter ipAddress in network_aaa, server ISE is set" + self.status = "failed" + return self + + if network_aaa.get("network"): + want_network_settings.get("network_aaa").update({ + "network": network_aaa.get("network") + }) + else: + self.msg = "missing parameter network in network_aaa" + self.status = "failed" + return self + + if network_aaa.get("protocol"): + want_network_settings.get("network_aaa").update({ + "protocol": + network_aaa.get("protocol") + }) + else: + self.msg = "missing parameter protocol in network_aaa" + self.status = "failed" + return self + + if network_aaa.get("servers"): + want_network_settings.get("network_aaa").update({ + "servers": + network_aaa.get("servers") + }) + else: + self.msg = "missing parameter servers in network_aaa" + self.status = "failed" + return self + + if network_aaa.get("sharedSecret"): + want_network_settings.get("network_aaa").update({ + "sharedSecret": + network_aaa.get("sharedSecret") + }) + else: + del want_network_settings["network_aaa"] + + clientAndEndpoint_aaa = network_management_details.get("clientAndEndpoint_aaa") + if clientAndEndpoint_aaa: + if clientAndEndpoint_aaa.get("ipAddress"): + want_network_settings.get("clientAndEndpoint_aaa").update({ + "ipAddress": + clientAndEndpoint_aaa.get("ipAddress") + }) + else: + if clientAndEndpoint_aaa.get("servers") == "ISE": + self.msg = "missing parameter ipAddress in clientAndEndpoint_aaa, \ + server ISE is set" + self.status = "failed" + return self + + if clientAndEndpoint_aaa.get("network"): + want_network_settings.get("clientAndEndpoint_aaa").update({ + "network": + clientAndEndpoint_aaa.get("network") + }) + else: + self.msg = "missing parameter network in clientAndEndpoint_aaa" + self.status = "failed" + return self + + if clientAndEndpoint_aaa.get("protocol"): + want_network_settings.get("clientAndEndpoint_aaa").update({ + "protocol": + clientAndEndpoint_aaa.get("protocol") + }) + else: + self.msg = "missing parameter protocol in clientAndEndpoint_aaa" + self.status = "failed" + return self + + if clientAndEndpoint_aaa.get("servers"): + want_network_settings.get("clientAndEndpoint_aaa").update({ + "servers": + clientAndEndpoint_aaa.get("servers") + }) + else: + self.msg = "missing parameter servers in clientAndEndpoint_aaa" + self.status = "failed" + return self + + if clientAndEndpoint_aaa.get("sharedSecret"): + want_network_settings.get("clientAndEndpoint_aaa").update({ + "sharedSecret": + clientAndEndpoint_aaa.get("sharedSecret") + }) + else: + del want_network_settings["clientAndEndpoint_aaa"] + + self.log("Network Playbook Details " + str(want_network)) + self.want.update({"wantNetwork": want_network}) + self.msg = "Collecting the network details from the playbook" + self.status = "success" + return self + + 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 + + Returns: + None + """ + + if config.get("GlobalPoolDetails"): + global_ippool = config.get("GlobalPoolDetails").get("settings").get("ippool")[0] + self.get_want_global_pool(global_ippool).check_return_status() + + if config.get("ReservePoolDetails"): + reserve_pool = config.get("ReservePoolDetails") + self.get_want_reserve_pool(reserve_pool).check_return_status() + + if config.get("NetworkManagementDetails"): + network_management_details = config.get("NetworkManagementDetails") \ + .get("settings") + self.get_want_network(network_management_details).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 update_global_pool(self, config): + """ + Update/Create Global Pool in DNAC with fields provided in DNAC + + Parameters: + config (list of dict) - Playbook details + + Returns: + None + """ + + name = config.get("GlobalPoolDetails") \ + .get("settings").get("ippool")[0].get("ipPoolName") + result_global_pool = self.result.get("response")[0].get("globalPool") + result_global_pool.get("response").update({name: {}}) + + # Check pool exist, if not create and return + if not self.have.get("globalPool").get("exists"): + pool_params = self.want.get("wantGlobal") + self.log(str(pool_params)) + response = self.dnac._exec( + family="network_settings", + function="create_global_pool", + params=pool_params, + ) + self.check_execution_response_status(response).check_return_status() + self.log("Global Pool Created Successfully") + result_global_pool.get("response").get(name) \ + .update({"globalPool Details": self.want.get("wantGlobal")}) + result_global_pool.get("msg").update({name: "Global Pool Created Successfully"}) + return + + # Pool exists, check update is required + obj_params = [ + ("settings", "settings"), + ] + if not self.requires_update(self.have.get("globalPool").get("details"), + self.want.get("wantGlobal"), obj_params): + self.log("Global pool doesn't requires an update") + result_global_pool.get("response").get(name).update({ + "DNAC params": + self.have.get("globalPool").get("details").get("settings").get("ippool")[0] + }) + result_global_pool.get("response").get(name).update({ + "Id": self.have.get("globalPool").get("id") + }) + result_global_pool.get("msg").update({ + name: "Global pool doesn't require an update" + }) + self.log(str(self.result)) + return + + self.log("Pool requires update") + # Pool Exists + pool_params = copy.deepcopy(self.want.get("wantGlobal")) + pool_params_ippool = pool_params.get("settings").get("ippool")[0] + pool_params_ippool.update({"id": self.have.get("globalPool").get("id")}) + self.log(str(pool_params)) + keys_to_remove = ["IpAddressSpace", "ipPoolCidr", "type"] + for key in keys_to_remove: + del pool_params["settings"]["ippool"][0][key] + + have_ippool = self.have.get("globalPool").get("details").get("settings").get("ippool")[0] + keys_to_update = ["dhcpServerIps", "dnsServerIps", "gateway"] + for key in keys_to_update: + if pool_params_ippool.get(key) is None: + pool_params_ippool[key] = have_ippool.get(key) + + self.log(str(pool_params)) + response = self.dnac._exec( + family="network_settings", + function="update_global_pool", + params=pool_params, + ) + + self.check_execution_response_status(response).check_return_status() + self.log("Global Pool Updated Successfully") + result_global_pool.get("response").get(name) \ + .update({"Id": self.have.get("globalPool").get("details").get("id")}) + result_global_pool.get("msg").update({name: "Global Pool Updated Successfully"}) + return + + 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. + 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. + + Returns: + 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 " + + str(self.have.get("reservePool").get("details"))) + self.log("Reserve Pool User Details " + + str(self.want.get("wantReserve"))) + + # Check pool exist, if not create and return + self.log(str(self.want.get("wantReserve").get("ipv4GlobalPool"))) + site_name = config.get("ReservePoolDetails").get("siteName") + reserve_params = self.want.get("wantReserve") + site_id = self.get_site_id(site_name) + reserve_params.update({"site_id": site_id}) + if not self.have.get("reservePool").get("exists"): + self.log(str(reserve_params)) + response = self.dnac._exec( + family="network_settings", + function="reserve_ip_subpool", + params=reserve_params, + ) + self.check_execution_response_status(response).check_return_status() + self.log("Ip Subpool Reservation Created Successfully") + result_reserve_pool.get("response").get(name) \ + .update({"reservePool Details": self.want.get("wantReserve")}) + result_reserve_pool.get("msg") \ + .update({name: "Ip Subpool Reservation Created Successfully"}) + return + + # Check update is required + obj_params = [ + ("name", "name"), + ("type", "type"), + ("ipv6AddressSpace", "ipv6AddressSpace"), + ("ipv4GlobalPool", "ipv4GlobalPool"), + ("ipv4Prefix", "ipv4Prefix"), + ("ipv4PrefixLength", "ipv4PrefixLength"), + ("ipv4GateWay", "ipv4GateWay"), + ("ipv4DhcpServers", "ipv4DhcpServers"), + ("ipv4DnsServers", "ipv4DnsServers"), + ("ipv6GateWay", "ipv6GateWay"), + ("ipv6DhcpServers", "ipv6DhcpServers"), + ("ipv6DnsServers", "ipv6DnsServers"), + ("ipv4TotalHost", "ipv4TotalHost"), + ("slaacSupport", "slaacSupport") + ] + if not self.requires_update(self.have.get("reservePool").get("details"), + self.want.get("wantReserve"), obj_params): + self.log("Reserved ip subpool doesn't require an update") + result_reserve_pool.get("response").get(name) \ + .update({"DNAC 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") \ + .update({name: "Reserve ip subpool doesn't require an update"}) + return + + 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 User Details" + str(self.want.get("wantReserve"))) + reserve_params.update({"id": self.have.get("reservePool").get("id")}) + response = self.dnac._exec( + family="network_settings", + function="update_reserve_ip_subpool", + params=reserve_params, + ) + self.check_execution_response_status(response).check_return_status() + self.log("Reserved Ip Subpool Updated Successfully") + result_reserve_pool['msg'] = "Reserved Ip Subpool Updated Successfully" + result_reserve_pool.get("response").get(name) \ + .update({"Reservation details": self.have.get("reservePool").get("details")}) + return + + def update_network(self, config): + """ + Update or create a network configuration in DNAC based on the provided playbook details. + + Parameters: + config (list of dict) - Playbook details containing Network Management information. + + Returns: + None + """ + + siteName = config.get("NetworkManagementDetails").get("siteName") + result_network = self.result.get("response")[2].get("network") + result_network.get("response").update({siteName: {}}) + obj_params = [ + ("settings", "settings"), + ("siteName", "siteName") + ] + + # Check update is required or not + if not self.requires_update(self.have.get("network").get("net_details"), + self.want.get("wantNetwork"), obj_params): + + self.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") + }) + 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 User Details" + str(self.want.get("wantNetwork"))) + + net_params = copy.deepcopy(self.want.get("wantNetwork")) + net_params.update({"site_id": self.have.get("network").get("site_id")}) + response = self.dnac._exec( + family="network_settings", + function='update_network', + params=net_params, + ) + self.log(str(response)) + self.check_execution_response_status(response).check_return_status() + self.log("Network has been changed Successfully") + result_network.get("msg") \ + .update({siteName: "Network Updated successfully"}) + result_network.get("response").get(siteName) \ + .update({"Network Details": self.want.get("wantNetwork").get("settings")}) + return + + def get_diff_merged(self, config): + """ + Update or create Global Pool, Reserve Pool, and + Network configurations in DNAC based on the playbook details + + Parameters: + config (list of dict) - Playbook details containing + Global Pool, Reserve Pool, and Network Management information. + + Returns: + self + """ + + if config.get("GlobalPoolDetails") is not None: + self.update_global_pool(config) + + if config.get("ReservePoolDetails") is not None: + self.update_reserve_pool(config) + + if config.get("NetworkManagementDetails") is not None: + self.update_network(config) + + return self + + def delete_reserve_pool(self, name): + """ + Delete a Reserve Pool by name in DNAC + + Parameters: + name (str) - The name of the Reserve Pool to be deleted. + + Returns: + self + """ + + reserve_pool_exists = self.have.get("reservePool").get("exists") + self.log("Reserved Ip Pool to be deleted " + + str(self.want.get("wantReserve").get("name"))) + + if not reserve_pool_exists: + self.msg = "Reserved Ip Subpool Not Found" + self.status = "failed" + return self + + _id = self.have.get("reservePool").get("id") + self.log("Reserve pool {0} id ".format(name) + str(_id)) + response = self.dnac._exec( + family="network_settings", + function="release_reserve_ip_subpool", + params={"id": _id}, + ) + self.check_execution_response_status(response).check_return_status() + executionid = response.get("executionId") + result_reserve_pool = self.result.get("response")[1].get("reservePool") + result_reserve_pool.get("response").update({name: {}}) + result_reserve_pool.get("response").get(name) \ + .update({"Execution Id": executionid}) + result_reserve_pool.get("msg") \ + .update({name: "Ip subpool reservation released successfully"}) + self.msg = "Reserve pool - {0} released successfully".format(name) + self.status = "success" + return self + + def delete_global_pool(self, name): + """ + Delete a Global Pool by name in DNAC + + Parameters: + name (str) - The name of the Global Pool to be deleted. + + Returns: + self + """ + + global_pool_exists = self.have.get("globalPool").get("exists") + if not global_pool_exists: + self.msg = "Global pool Not Found" + self.status = "failed" + return self + + response = self.dnac._exec( + family="network_settings", + function="delete_global_ip_pool", + params={"id": self.have.get("globalPool").get("id")}, + ) + + # Check the execution status + self.check_execution_response_status(response).check_return_status() + executionid = response.get("executionId") + + # Update result information + result_global_pool = self.result.get("response")[0].get("globalPool") + result_global_pool.get("response").update({name: {}}) + result_global_pool.get("response").get(name).update({"Execution Id": executionid}) + result_global_pool.get("msg").update({name: "Pool deleted successfully"}) + self.msg = "Global pool - {0} deleted successfully".format(name) + self.status = "success" + return self + + def get_diff_deleted(self, config): + """ + Delete Reserve Pool and Global Pool in DNAC based on playbook details. + + Parameters: + config (list of dict) - Playbook details + + Returns: + self + """ + + if config.get("ReservePoolDetails") is not None: + name = config.get("ReservePoolDetails").get("name") + self.delete_reserve_pool(name).check_return_status() + + if config.get("GlobalPoolDetails") is not None: + name = config.get("GlobalPoolDetails") \ + .get("settings").get("ippool")[0].get("ipPoolName") + self.delete_global_pool(name).check_return_status() + + return self + + def reset_values(self): + """ + Reset all neccessary attributes to default values + + Parameters: + None + + Returns: + None + """ + + self.have.clear() + self.want.clear() + return + + +def main(): + """main entry point for module execution""" + + # Define the specification for module arguments + element_spec = { + "dnac_host": {"type": 'str', "required": True}, + "dnac_port": {"type": 'str', "default": '443'}, + "dnac_username": {"type": 'str', "default": 'admin', "aliases": ['user']}, + "dnac_password": {"type": 'str', "no_log": True}, + "dnac_verify": {"type": 'bool', "default": 'True'}, + "dnac_version": {"type": 'str', "default": '2.2.3.3'}, + "dnac_debug": {"type": 'bool', "default": False}, + "dnac_log": {"type": 'bool', "default": False}, + "config": {"type": 'list', "required": True, "elements": 'dict'}, + "state": {"default": 'merged', "choices": ['merged', 'deleted']}, + "validate_response_schema": {"type": 'bool', "default": True}, + } + + # Create an AnsibleModule object with argument specifications + module = AnsibleModule(argument_spec=element_spec, supports_check_mode=False) + dnac_network = DnacNetwork(module) + state = dnac_network.params.get("state") + if state not in dnac_network.supported_states: + dnac_network.status = "invalid" + dnac_network.msg = "State {0} is invalid".format(state) + dnac_network.check_return_status() + + dnac_network.validate_input().check_return_status() + + for config in dnac_network.config: + dnac_network.reset_values() + dnac_network.get_have(config).check_return_status() + dnac_network.get_want(config).check_return_status() + dnac_network.get_diff_state_apply[state](config).check_return_status() + + module.exit_json(**dnac_network.result) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/pnp_intent.py b/plugins/modules/pnp_intent.py index 02c89721a6..6ce4a54256 100644 --- a/plugins/modules/pnp_intent.py +++ b/plugins/modules/pnp_intent.py @@ -609,97 +609,78 @@ } """ -import copy from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( - DNACSDK, + DnacBase, validate_list_of_dicts, - log, - get_dict_result, + get_dict_result ) -class DnacPnp: +class DnacPnp(DnacBase): def __init__(self, module): - self.module = module - self.params = module.params - self.config = copy.deepcopy(module.params.get("config")) - self.have = [] - self.want = [] - self.diff = [] - self.validated = [] - dnac_params = self.get_dnac_params(self.params) - log(str(dnac_params)) - self.dnac = DNACSDK(params=dnac_params) - self.log = dnac_params.get("dnac_log") - - self.result = dict(changed=False, diff=[], response=[], warnings=[]) - - def get_state(self): - return self.params.get("state") + super().__init__(module) def validate_input(self): - pnp_spec = dict( - template_name=dict(required=True, type='str'), - project_name=dict(required=False, type='str', default="Onboarding Configuration"), - site_name=dict(required=True, type='str'), - image_name=dict(required=True, type='str'), - golden_image=dict(required=False, type='bool'), - deviceInfo=dict(required=True, type='dict'), - pnp_type=dict(required=False, type=str, default="Default") - ) - if self.config: - msg = None + """ + Validate the fields provided in the playbook + """ + + if not self.config: + self.msg = "config not available in playbook for validation" + self.status = "success" + return self + + pnp_spec = { + 'template_name': {'type': 'str', 'required': True}, + 'project_name': {'type': 'str', 'required': False, + 'default': 'Onboarding Configuration'}, + 'site_name': {'type': 'str', 'required': True}, + 'image_name': {'type': 'str', 'required': True}, + 'golden_image': {'type': 'bool', 'required': False}, + 'deviceInfo': {'type': 'dict', 'required': True}, + 'pnp_type': {'type': 'str', 'required': False, 'default': 'Default'}, + } + + # Validate pnp params + valid_pnp, invalid_params = validate_list_of_dicts( + self.config, pnp_spec + ) - # Validate template params - if self.log: - log(str(self.config)) - valid_pnp, invalid_params = validate_list_of_dicts( - self.config, pnp_spec - ) + if invalid_params: + self.msg = "Invalid parameters in playbook: {0}".format( + "\n".join(invalid_params)) + self.status = "failed" + return self - if invalid_params: - msg = "Invalid parameters in playbook: {0}".format( - "\n".join(invalid_params) - ) - self.module.fail_json(msg=msg) - - self.validated = valid_pnp - - if self.log: - log(str(valid_pnp)) - log(str(self.validated)) - - def get_dnac_params(self, params): - dnac_params = dict( - dnac_host=params.get("dnac_host"), - dnac_port=params.get("dnac_port"), - dnac_username=params.get("dnac_username"), - dnac_password=params.get("dnac_password"), - dnac_verify=params.get("dnac_verify"), - dnac_debug=params.get("dnac_debug"), - dnac_log=params.get("dnac_log") - ) - return dnac_params + self.validated_config = valid_pnp + self.log(str(valid_pnp)) + self.msg = "Successfully validated input" + self.status = "success" + return self def site_exists(self): + + """ + Check whether the site exists or not + """ + site_exists = False site_id = None response = None + try: - response = self.dnac._exec( + response = self.dnac_apply['exec']( family="sites", function='get_site', params={"name": self.want.get("site_name")}, ) - except Exception as e: + except Exception: self.module.fail_json(msg="Site not found", response=[]) if response: - if self.log: - log(str(response)) - + self.log(str(response)) site = response.get("response") site_id = site[0].get("id") site_exists = True @@ -707,90 +688,109 @@ def site_exists(self): return (site_exists, site_id) def get_pnp_params(self, params): - pnp_params = {} - pnp_params['_id'] = params.get('_id') - pnp_params['deviceInfo'] = params.get('deviceInfo') - pnp_params['runSummaryList'] = params.get('runSummaryList') - pnp_params['systemResetWorkflow'] = params.get('systemResetWorkflow') - pnp_params['systemWorkflow'] = params.get('systemWorkflow') - pnp_params['tenantId'] = params.get('tenantId') - pnp_params['version'] = params.get('device_version') - pnp_params['workflow'] = params.get('workflow') - pnp_params['workflowParameters'] = params.get('workflowParameters') + """ + Store pnp parameters from the playbook for pnp processing in DNAC + """ + + 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') + } return pnp_params def get_image_params(self, params): - image_params = dict( - image_name=params.get("image_name"), - is_tagged_golden=params.get("golden_image"), - ) + """ + Get image name and the confirmation whether it's tagged golden or not + """ + + image_params = { + 'image_name': params.get('image_name'), + 'is_tagged_golden': params.get('golden_image') + } return image_params def get_claim_params(self): - imageinfo = dict( - imageId=self.have.get("image_id") - ) - configinfo = dict( - configId=self.have.get("template_id"), - configParameters=[dict( - key="", - value="" - )] - ) - claim_params = dict( - deviceId=self.have.get("device_id"), - siteId=self.have.get("site_id"), - type=self.want.get("pnp_type"), - hostname=self.want.get("hostname"), - imageInfo=imageinfo, - configInfo=configinfo, - ) + + """ + Get the paramters needed for claiming + """ + + imageinfo = { + 'imageId': self.have.get('image_id') + } + + configinfo = { + 'configId': self.have.get('template_id'), + 'configParameters': [ + { + 'key': '', + 'value': '' + } + ] + } + + claim_params = { + 'deviceId': self.have.get('device_id'), + 'siteId': self.have.get('site_id'), + 'type': self.want.get('pnp_type'), + 'hostname': self.want.get('hostname'), + 'imageInfo': imageinfo, + 'configInfo': configinfo, + } return claim_params def get_have(self): - have = {} + """ + Get the current image, template and site details from the DNAC + """ + + have = {} if self.params.get("state") == "merged": # check if given image exists, if exists store image_id - image_response = self.dnac._exec( + image_response = self.dnac_apply['exec']( family="software_image_management_swim", function='get_software_image_details', params=self.want.get("image_params"), ) - if self.log: - log(str(image_response)) + self.log(str(image_response)) image_list = image_response.get("response") if len(image_list) == 1: have["image_id"] = image_list[0].get("imageUuid") - if self.log: - log("Image Id: " + str(have["image_id"])) + 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 - template_list = self.dnac._exec( + template_list = self.dnac_apply['exec']( family="configuration_templates", function='gets_the_templates_available', params={"project_names": self.want.get("project_name")}, ) - if self.log: - log(str(template_list)) + 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")) + template_details = get_dict_result(template_list, + 'name', self.want.get("template_name")) if template_details: have["template_id"] = template_details.get("templateId") - if self.log: - log("Template Id: " + str(have["template_id"])) + self.log("Template Id: " + str(have["template_id"])) else: self.module.fail_json(msg="Template not found", response=[]) else: @@ -804,52 +804,66 @@ def get_have(self): if site_exists: have["site_id"] = site_id - if self.log: - log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) - log("Site Name:" + str(site_name)) + self.log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) + self.log("Site Name:" + str(site_name)) # check if given device exists in pnp inventory, store device Id - device_response = self.dnac._exec( + device_response = self.dnac_apply['exec']( family="device_onboarding_pnp", function='get_device_list', params={"serial_number": self.want.get("serial_number")} ) - if self.log: - log(str(device_response)) + self.log(str(device_response)) if device_response and (len(device_response) == 1): have["device_id"] = device_response[0].get("id") have["device_found"] = True - if self.log: - log("Device Id: " + str(have["device_id"])) + self.log("Device Id: " + str(have["device_id"])) else: have["device_found"] = False - + self.msg = "Successfully collected all project and template \ + parameters from dnac for comparison" + self.status = "success" self.have = have - def get_want(self): - for params in self.validated: - want = dict( - image_params=self.get_image_params(params), - pnp_params=self.get_pnp_params(params), - pnp_type=params.get("pnp_type"), - site_name=params.get("site_name"), - serial_number=params.get("deviceInfo").get("serialNumber"), - hostname=params.get("deviceInfo").get("hostname"), - project_name=params.get("project_name"), - template_name=params.get("template_name") - ) + 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 + """ + + self.want = { + 'image_params': self.get_image_params(config), + 'pnp_params': self.get_pnp_params(config), + 'pnp_type': config.get('pnp_type'), + 'site_name': config.get('site_name'), + 'serial_number': config.get('deviceInfo').get('serialNumber'), + 'hostname': config.get('deviceInfo').get('hostname'), + 'project_name': config.get('project_name'), + 'template_name': config.get('template_name') + } - self.want = want + self.msg = "Successfully collected all parameters from playbook " + \ + "for comparison" + self.status = "success" - def get_diff_merge(self): + 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 if not self.have.get("device_found"): - log("Adding device to pnp database") - response = self.dnac._exec( + 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"), @@ -857,20 +871,18 @@ def get_diff_merge(self): ) self.have["device_id"] = response.get("id") - if self.log: - log(str(response)) - log(self.have.get("device_id")) + self.log(str(response)) + self.log(self.have.get("device_id")) claim_params = self.get_claim_params() - claim_response = self.dnac._exec( + claim_response = self.dnac_apply['exec']( family="device_onboarding_pnp", function='claim_a_device_to_a_site', op_modifies=True, params=claim_params, ) - if self.log: - log(str(claim_response)) + self.log(str(claim_response)) if claim_response.get("response") == "Device Claimed": self.result['changed'] = True @@ -880,19 +892,20 @@ def get_diff_merge(self): else: self.module.fail_json(msg="Device Claim Failed", response=claim_response) - def get_diff_delete(self): + return self + + def get_diff_deleted(self): if self.have.get("device_found"): try: - response = self.dnac._exec( + response = self.dnac_apply['exec']( family="device_onboarding_pnp", function="delete_device_by_id_from_pnp", op_modifies=True, params={"id": self.have.get("device_id")}, ) - if self.log: - log(str(response)) + self.log(str(response)) if response.get("deviceInfo").get("state") == "Deleted": self.result['changed'] = True @@ -911,42 +924,45 @@ def get_diff_delete(self): else: self.module.fail_json(msg="Device Not Found", response=[]) + return self + def main(): - """ main entry point for module execution + + """ + main entry point for module execution """ - element_spec = dict( - dnac_host=dict(required=True, type='str'), - dnac_port=dict(type='str', default='443'), - dnac_username=dict(type='str', default='admin', aliases=["user"]), - dnac_password=dict(type='str', no_log=True), - dnac_verify=dict(type='bool', default='True'), - dnac_version=dict(type="str", default="2.2.3.3"), - dnac_debug=dict(type='bool', default=False), - dnac_log=dict(type='bool', default=False), - validate_response_schema=dict(type="bool", default=True), - config=dict(required=True, type='list', elements='dict'), - state=dict( - default='merged', - choices=['merged', 'deleted'] - ) - ) + element_spec = {'dnac_host': {'required': True, 'type': 'str'}, + 'dnac_port': {'type': 'str', 'default': '443'}, + 'dnac_username': {'type': 'str', 'default': 'admin', 'aliases': ['user']}, + 'dnac_password': {'type': 'str', 'no_log': True}, + 'dnac_verify': {'type': 'bool', 'default': 'True'}, + 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, + 'dnac_debug': {'type': 'bool', 'default': False}, + 'dnac_log': {'type': 'bool', 'default': False}, + 'validate_response_schema': {'type': 'bool', 'default': True}, + 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, + 'state': {'default': 'merged', 'choices': ['merged', 'deleted']} + } module = AnsibleModule(argument_spec=element_spec, supports_check_mode=False) dnac_pnp = DnacPnp(module) - dnac_pnp.validate_input() - state = dnac_pnp.get_state() - dnac_pnp.get_want() - dnac_pnp.get_have() + state = dnac_pnp.params.get("state") + if state not in dnac_pnp.supported_states: + dnac_pnp.status = "invalid" + dnac_pnp.msg = "State {0} is invalid".format(state) + dnac_pnp.check_return_status() - if state == "merged": - dnac_pnp.get_diff_merge() + dnac_pnp.validate_input().check_return_status() - elif state == "deleted": - dnac_pnp.get_diff_delete() + for config in dnac_pnp.validated_config: + dnac_pnp.reset_values() + dnac_pnp.get_want(config).check_return_status() + dnac_pnp.get_have().check_return_status() + dnac_pnp.get_diff_state_apply[state]().check_return_status() module.exit_json(**dnac_pnp.result) diff --git a/plugins/modules/site_intent.py b/plugins/modules/site_intent.py index 887db6b8c2..4671385443 100644 --- a/plugins/modules/site_intent.py +++ b/plugins/modules/site_intent.py @@ -7,7 +7,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -__author__ = ("Madhan Sankaranarayanan, Rishita Chowdhary") +__author__ = ("Madhan Sankaranarayanan, Rishita Chowdhary, Abhishek Maheshwari") DOCUMENTATION = r""" --- @@ -214,13 +214,12 @@ from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( - DNACSDK, + DnacBase, validate_list_of_dicts, log, get_dict_result, dnac_compare_equality, ) -import copy floor_plan = { '57057': 'CUBES AND WALLED OFFICES', @@ -231,64 +230,47 @@ } -class DnacSite: +class DnacSite(DnacBase): + """Class containing member attributes for site intent module""" def __init__(self, module): - self.module = module - self.params = module.params - self.config = copy.deepcopy(module.params.get("config")) - self.have = {} - self.want_create = {} - self.diff_create = [] - self.validated = [] - dnac_params = self.get_dnac_params(self.params) - log(str(dnac_params)) - self.dnac = DNACSDK(params=dnac_params) - self.log = dnac_params.get("dnac_log") - - self.result = dict(changed=False, diff=[], response=[], warnings=[]) - - def get_state(self): - return self.params.get("state") + super().__init__(module) + self.supported_states = ["merged", "deleted"] def validate_input(self): + """Validate the fields provided in the playbook""" + + if not self.config: + self.msg = "config not available in playbook for validattion" + self.status = "success" + return self + temp_spec = dict( type=dict(required=False, type='str'), site=dict(required=True, type='dict'), ) - if self.config: - msg = None - # Validate site params - valid_temp, invalid_params = validate_list_of_dicts( - self.config, temp_spec + # Validate site params + valid_temp, invalid_params = validate_list_of_dicts( + self.config, temp_spec + ) + + if invalid_params: + self.msg = "Invalid parameters in playbook: {0}".format( + "\n".join(invalid_params) ) + self.status = "failed" + return self - if invalid_params: - msg = "Invalid parameters in playbook: {0}".format( - "\n".join(invalid_params) - ) - self.module.fail_json(msg=msg) - - self.validated = valid_temp - - if self.log: - log(str(valid_temp)) - log(str(self.validated)) - - def get_dnac_params(self, params): - dnac_params = dict( - dnac_host=params.get("dnac_host"), - dnac_port=params.get("dnac_port"), - dnac_username=params.get("dnac_username"), - dnac_password=params.get("dnac_password"), - dnac_verify=params.get("dnac_verify"), - dnac_debug=params.get("dnac_debug"), - dnac_log=params.get("dnac_log") - ) - return dnac_params + self.validated_config = valid_temp + log(str(valid_temp)) + self.msg = "Successfully validated input" + self.status = "success" + return self def get_current_site(self, site): + """Get the current site info""" + site_info = {} location = get_dict_result(site[0].get("additionalInfo"), 'nameSpace', "Location") @@ -335,12 +317,13 @@ def get_current_site(self, site): siteId=site[0].get("id") ) - if self.log: - log(str(current_site)) + log(str(current_site)) return current_site def site_exists(self): + """Check if the site exists""" + site_exists = False current_site = {} response = None @@ -352,23 +335,22 @@ def site_exists(self): ) except Exception as e: - if self.log: - log("The input site is not valid or site is not present.") + log("The input site is not valid or site is not present.") if response: - if self.log: - log(str(response)) + log(str(response)) response = response.get("response") current_site = self.get_current_site(response) site_exists = True - if self.log: - log(str(self.validated)) + log(str(self.validated_config)) return (site_exists, current_site) def get_site_params(self, params): + """Store the site related parameters""" + site = params.get("site") typeinfo = params.get("type") @@ -383,23 +365,25 @@ def get_site_params(self, params): return site_params def get_site_name(self, site): + """Get and Return the site name""" + site_type = site.get("type") parent_name = site.get("site").get(site_type).get("parentName") name = site.get("site").get(site_type).get("name") site_name = '/'.join([parent_name, name]) - if self.log: - log(site_name) + log(site_name) return site_name def site_requires_update(self): + """Check if the site requires updates""" + requested_site = self.want.get("site_params") current_site = self.have.get("current_site") - if self.log: - log("Current Site: " + str(current_site)) - log("Requested Site: " + str(requested_site)) + log("Current Site: " + str(current_site)) + log("Requested Site: " + str(requested_site)) obj_params = [ ("type", "type"), @@ -418,13 +402,14 @@ def get_execution_details(self, execid): params={"execution_id": execid} ) - if self.log: - log(str(response)) + log(str(response)) if response and isinstance(response, dict): return response - def get_have(self): + def get_have(self, config): + """Get the site details from DNAC""" + site_exists = False current_site = None have = {} @@ -432,8 +417,7 @@ def get_have(self): # check if given site exits, if exists store current site info (site_exists, current_site) = self.site_exists() - if self.log: - log("Site Exists: " + str(site_exists) + "\n Current Site:" + str(current_site)) + log("Site Exists: " + str(site_exists) + "\n Current Site:" + str(current_site)) if site_exists: have["site_id"] = current_site.get("siteId") @@ -442,18 +426,26 @@ def get_have(self): self.have = have - def get_want(self): + return self + + def get_want(self, config): + """Get all the site related information from playbook + that is needed to be created in DNAC""" + want = {} - for site in self.validated: - want = dict( - site_params=self.get_site_params(site), - site_name=self.get_site_name(site), - ) + want = dict( + site_params=self.get_site_params(config), + site_name=self.get_site_name(config), + ) self.want = want - def get_diff_merge(self): + return self + + def get_diff_merged(self, config): + """Update/Create site info in DNAC with fields provided in DNAC""" + site_updated = False site_created = False @@ -519,7 +511,11 @@ def get_diff_merge(self): self.result['msg'] = "Site Created Successfully" self.result['response'].update({"siteId": current_site.get('site_id')}) - def get_diff_delete(self): + return self + + def get_diff_deleted(self, config): + """Call DNAC API to delete sites with provided inputs""" + site_exists = self.have.get("site_exists") if site_exists: @@ -548,42 +544,44 @@ def get_diff_delete(self): else: self.module.fail_json(msg="Site Not Found", response=[]) + return self + def main(): """ main entry point for module execution """ - element_spec = dict( - dnac_host=dict(required=True, type='str'), - dnac_port=dict(type='str', default='443'), - dnac_username=dict(type='str', default='admin', aliases=["user"]), - dnac_password=dict(type='str', no_log=True), - dnac_verify=dict(type='bool', default='True'), - dnac_version=dict(type="str", default="2.2.3.3"), - dnac_debug=dict(type='bool', default=False), - dnac_log=dict(type='bool', default=False), - validate_response_schema=dict(type="bool", default=True), - config=dict(required=True, type='list', elements='dict'), - state=dict( - default='merged', - choices=['merged', 'deleted']), - ) + element_spec = {'dnac_host': {'required': True, 'type': 'str'}, + 'dnac_port': {'type': 'str', 'default': '443'}, + 'dnac_username': {'type': 'str', 'default': 'admin', 'aliases': ['user']}, + 'dnac_password': {'type': 'str', 'no_log': True}, + 'dnac_verify': {'type': 'bool', 'default': 'True'}, + 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, + 'dnac_debug': {'type': 'bool', 'default': False}, + 'dnac_log': {'type': 'bool', 'default': False}, + 'validate_response_schema': {'type': 'bool', 'default': True}, + 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, + 'state': {'default': 'merged', 'choices': ['merged', 'deleted']} + } module = AnsibleModule(argument_spec=element_spec, supports_check_mode=False) dnac_site = DnacSite(module) - dnac_site.validate_input() - state = dnac_site.get_state() + state = dnac_site.params.get("state") - dnac_site.get_want() - dnac_site.get_have() + if state not in dnac_site.supported_states: + dnac_site.status = "invalid" + dnac_site.msg = "State {0} is invalid".format(state) + dnac_site.check_return_status() - if state == "merged": - dnac_site.get_diff_merge() + dnac_site.validate_input().check_return_status() - elif state == "deleted": - dnac_site.get_diff_delete() + for config in dnac_site.validated_config: + dnac_site.reset_values() + dnac_site.get_want(config).check_return_status() + dnac_site.get_have(config).check_return_status() + dnac_site.get_diff_state_apply[state](config).check_return_status() module.exit_json(**dnac_site.result) diff --git a/plugins/modules/swim_intent.py b/plugins/modules/swim_intent.py index ca173fb440..58636409b1 100644 --- a/plugins/modules/swim_intent.py +++ b/plugins/modules/swim_intent.py @@ -7,7 +7,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -__author__ = ("Madhan Sankaranarayanan, Rishita Chowdhary") +__author__ = ("Madhan Sankaranarayanan, Rishita Chowdhary, Abhishek Maheshwari") DOCUMENTATION = r""" --- @@ -28,7 +28,13 @@ - cisco.dnac.intent_params author: Madhan Sankaranarayanan (@madhansansel) Rishita Chowdhary (@rishitachowdhary) + Abhishek Maheshwari (@abmahesh) options: + state: + description: The state of DNAC after module completion. + type: str + choices: [ merged ] + default: merged config: description: List of details of SWIM image being managed type: list @@ -259,9 +265,8 @@ """ -import copy from ansible_collections.cisco.dnac.plugins.module_utils.dnac import ( - DNACSDK, + DnacBase, validate_list_of_dicts, log, get_dict_result, @@ -269,68 +274,65 @@ from ansible.module_utils.basic import AnsibleModule -class DnacSwims: +class DnacSwims(DnacBase): + """Class containing member attributes for Swim intent module""" def __init__(self, module): - self.module = module - self.params = module.params - self.config = copy.deepcopy(module.params.get("config")) - self.have = {} - self.want_create = {} - self.diff_create = [] - self.validated = [] - dnac_params = self.get_dnac_params(self.params) - log(str(dnac_params)) - self.dnac = DNACSDK(params=dnac_params) - self.log = dnac_params.get("dnac_log") - - self.result = dict(changed=False, diff=[], response=[], warnings=[]) + super().__init__(module) + self.supported_states = ["merged"] def validate_input(self): + """Validate the fields provided in the playbook""" + + if not self.config: + self.msg = "config not available in playbook for validattion" + self.status = "success" + return self + temp_spec = dict( importImageDetails=dict(type='dict'), taggingDetails=dict(type='dict'), imageDistributionDetails=dict(type='dict'), imageActivationDetails=dict(type='dict'), ) - if self.config: - msg = None - # Validate site params - valid_temp, invalid_params = validate_list_of_dicts( - self.config, temp_spec - ) - if invalid_params: - msg = "Invalid parameters in playbook: {0}".format( - "\n".join(invalid_params) - ) - self.module.fail_json(msg=msg) - - self.validated = valid_temp - if self.log: - log(str(valid_temp)) - log(str(self.validated)) - - def get_dnac_params(self, params): - dnac_params = dict( - dnac_host=params.get("dnac_host"), - dnac_port=params.get("dnac_port"), - dnac_username=params.get("dnac_username"), - dnac_password=params.get("dnac_password"), - dnac_verify=params.get("dnac_verify"), - dnac_debug=params.get("dnac_debug"), - dnac_log=params.get("dnac_log") + + # Validate swim params + valid_temp, invalid_params = validate_list_of_dicts( + self.config, temp_spec ) - return dnac_params + if invalid_params: + self.msg = "Invalid parameters in playbook: {0}".format( + "\n".join(invalid_params) + ) + self.status = "failed" + return self + + self.validated_config = valid_temp + self.log(str(valid_temp)) + self.msg = "Successfully validated input" + self.status = "success" + return self + + def get_task_details(self, task_id): + """ + Args: + - self: The reference to the class instance. + - task_id (str): The unique ID of the task whose details are to be retrieved. + Returns: + - dict or None: A dictionary containing the details of the task if found, or None if the task is not found. + Description: + This method sends a request to the DNAC to retrieve the details of a task identified by its unique ID. + The 'id' parameter should be a valid task ID. If the task is found, its details are extracted from the response + and returned as a dictionary. If the task is not found or an error occurs during retrieval, None is returned. + """ - def get_task_details(self, id): result = None response = self.dnac._exec( family="task", function='get_task_by_id', - params={"task_id": id}, + params={"task_id": task_id}, ) - if self.log: - log(str(response)) + log(str(response)) if isinstance(response, dict): result = response.get("response") @@ -338,6 +340,19 @@ def get_task_details(self, id): return result def site_exists(self): + """ + Args: + - self: The reference to the class instance. + Returns: + - tuple: A tuple containing two values: + - site_exists (bool): A boolean indicating whether the site exists (True) or not (False). + - site_id (str or None): The ID of the site if it exists, or None if the site is not found. + Description: + This method checks the existence of a site in the DNAC. If the site is found,it sets 'site_exists' to True, + retrieves the site's ID, and returns both values in a tuple. If the site does not exist, 'site_exists' is set + to False, and 'site_id' is None. If an exception occurs during the site lookup, an exception is raised. + """ + site_exists = False site_id = None response = None @@ -351,8 +366,7 @@ def site_exists(self): self.module.fail_json(msg="Site not found") if response: - if self.log: - log(str(response)) + log(str(response)) site = response.get("response") site_id = site[0].get("id") @@ -361,66 +375,113 @@ def site_exists(self): return (site_exists, site_id) def get_image_id(self, name): - # check if given image exists, if exists store image_id + """ + Retrieve the unique image ID based on the provided image name. + Args: + name (str): The name of the software image to search for. + Returns: + str: The unique image ID (UUID) corresponding to the given image name. + Raises: + AnsibleFailJson: If the image is not found in the response. + Description: + This function sends a request to Cisco DNAC to retrieve details about a software image based on its name. + It extracts and returns the image ID if a single matching image is found. If no image or multiple + images are found with the same name, it raises an exception. + """ + image_response = self.dnac._exec( family="software_image_management_swim", function='get_software_image_details', params={"image_name": name}, ) - if self.log: - log(str(image_response)) + log(str(image_response)) image_list = image_response.get("response") if (len(image_list) == 1): image_id = image_list[0].get("imageUuid") - if self.log: - log("Image Id: " + str(image_id)) + log("Image Id: " + str(image_id)) else: self.module.fail_json(msg="Image not found", response=image_response) return image_id def get_device_id(self, params): + """ + Retrieve the unique device ID based on the provided parameters. + Args: + params (dict): A dictionary containing parameters to filter devices. + Returns: + str: The unique device ID corresponding to the filtered device. + Raises: + AnsibleFailJson: If the device is not found in the response. + Description: + This function sends a request to Cisco DNA Center to retrieve a list of devices based on the provided + filtering parameters. If a single matching device is found, it extracts and returns the device ID. If + no device or multiple devices match the criteria, it raises an exception. + """ + response = self.dnac._exec( family="devices", function='get_device_list', params=params, ) - if self.log: - log(str(response)) + log(str(response)) device_list = response.get("response") if (len(device_list) == 1): device_id = device_list[0].get("id") - if self.log: - log("Device Id: " + str(device_id)) + log("Device Id: " + str(device_id)) else: self.module.fail_json(msg="Device not found", response=response) return device_id def get_device_family_identifier(self, family_name): + """ + Retrieve and store the device family identifier based on the provided family name. + Args: + family_name (str): The name of the device family for which to retrieve the identifier. + Returns: + None + Raises: + AnsibleFailJson: If the family name is not found in the response. + Description: + This function sends a request to Cisco DNA Center to retrieve a list of device family identifiers.It then + searches for a specific family name within the response and stores its associated identifier. If the family + name is found, the identifier is stored; otherwise, an exception is raised. + """ + have = {} response = self.dnac._exec( family="software_image_management_swim", function='get_device_family_identifiers', ) - if self.log: - log(str(response)) + log(str(response)) device_family_db = response.get("response") if device_family_db: device_family_details = get_dict_result(device_family_db, 'deviceFamily', family_name) if device_family_details: device_family_identifier = device_family_details.get("deviceFamilyIdentifier") have["device_family_identifier"] = device_family_identifier - if self.log: - log("Family device indentifier:" + str(device_family_identifier)) + log("Family device indentifier:" + str(device_family_identifier)) else: self.module.fail_json(msg="Family Device Name not found", response=[]) self.have.update(have) def get_have(self): + """ + Retrieve and store various software image and device details based on user-provided information. + Returns: + self: The current instance of the class with updated 'have' attributes. + Raises: + AnsibleFailJson: If required image or device details are not provided. + Description: + This function populates the 'have' dictionary with details related to software images, site information, + device families, distribution devices, and activation devices based on user-provided data in the 'want' dictionary. + It validates and retrieves the necessary information from Cisco DNAC to support later actions. + """ + if self.want.get("tagging_details"): have = {} tagging_details = self.want.get("tagging_details") @@ -442,13 +503,11 @@ def get_have(self): (site_exists, site_id) = self.site_exists() if site_exists: have["site_id"] = site_id - if self.log: - log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) + log("Site Exists: " + str(site_exists) + "\n Site_id:" + str(site_id)) else: # For global site, use -1 as siteId have["site_id"] = "-1" - if self.log: - log("Site Name not given by user. Using global site.") + log("Site Name not given by user. Using global site.") self.have.update(have) # check if given device family name exists, store indentifier value @@ -503,28 +562,58 @@ def get_have(self): have["activation_device_id"] = device_id self.have.update(have) - def get_want(self): + return self + + def get_want(self, config): + """ + Retrieve and store import, tagging, distribution, and activation details from playbook configuration. + Args: + config (dict): The configuration dictionary containing image import and other details. + Returns: + self: The current instance of the class with updated 'want' attributes. + Raises: + AnsibleFailJson: If an incorrect import type is specified. + Description: + This function parses the playbook configuration to extract information related to image + import, tagging, distribution, and activation. It stores these details in the 'want' dictionary + for later use in the Ansible module. + """ + want = {} - for image in self.validated: - if image.get("importImageDetails"): - want["import_image"] = True - want["import_type"] = image.get("importImageDetails").get("type").lower() - if want["import_type"] == "url": - want["url_import_details"] = image.get("importImageDetails").get("urlDetails") - elif want["import_type"] == "local": - want["local_import_details"] = image.get("importImageDetails").get("localImageDetails") - else: - self.module.fail_json(msg="Incorrect import type. Supported Values: local or url") + if config.get("importImageDetails"): + want["import_image"] = True + want["import_type"] = config.get("importImageDetails").get("type").lower() + if want["import_type"] == "url": + want["url_import_details"] = config.get("importImageDetails").get("urlDetails") + elif want["import_type"] == "local": + want["local_import_details"] = config.get("importImageDetails").get("localImageDetails") + else: + self.module.fail_json(msg="Incorrect import type. Supported Values: local or url") - want["tagging_details"] = image.get("taggingDetails") - want["distribution_details"] = image.get("imageDistributionDetails") - want["activation_details"] = image.get("imageActivationDetails") + want["tagging_details"] = config.get("taggingDetails") + want["distribution_details"] = config.get("imageDistributionDetails") + want["activation_details"] = config.get("imageActivationDetails") self.want = want - if self.log: - log(str(self.want)) + log(str(self.want)) + + return self def get_diff_import(self): + """ + Check the image import type and fetch the image ID for the imported image for further use. + Args: + None + Returns: + self: The current instance of the class with updated 'have' attributes. + Description: + This function checks the type of image import (URL or local) and proceeds with the import operation accordingly. + It then monitors the import task's progress and updates the 'result' dictionary. If the operation is successful, + 'changed' is set to True. + Additionally, if tagging, distribution, or activation details are provided, it fetches the image ID for the + imported image and stores it in the 'have' dictionary for later use. + """ + if not self.want.get("import_image"): return @@ -559,8 +648,7 @@ def get_diff_import(self): file_paths=[('file_path', 'file')], ) - if self.log: - log(str(response)) + log(str(response)) task_details = {} task_id = response.get("response").get("taskId") @@ -589,7 +677,23 @@ def get_diff_import(self): image_id = self.get_image_id(image_name) self.have["imported_image_id"] = image_id + return self + def get_diff_tagging(self): + """ + Tag or untag a software image as golden based on provided tagging details. + Args: + None + Returns: + None + Description: + This function tags or untags a software image as a golden image in Cisco DNAC based on the provided + tagging details. The tagging action is determined by the value of the 'tagging' attribute + in the 'tagging_details' dictionary.If 'tagging' is True, the image is tagged as golden, and if 'tagging' + is False, the golden tag is removed. The function sends the appropriate request to Cisco DNAC and updates the + task details in the 'result' dictionary. If the operation is successful, 'changed' is set to True. + """ + tagging_details = self.want.get("tagging_details") tag_image_golden = tagging_details.get("tagging") @@ -600,8 +704,7 @@ def get_diff_tagging(self): deviceFamilyIdentifier=self.have.get("device_family_identifier"), deviceRole=tagging_details.get("deviceRole") ) - if self.log: - log("Image params for tagging image as golden:" + str(image_params)) + log("Image params for tagging image as golden:" + str(image_params)) response = self.dnac._exec( family="software_image_management_swim", @@ -617,8 +720,7 @@ def get_diff_tagging(self): device_family_identifier=self.have.get("device_family_identifier"), device_role=tagging_details.get("deviceRole") ) - if self.log: - log("Image params for un-tagging image as golden:" + str(image_params)) + log("Image params for un-tagging image as golden:" + str(image_params)) response = self.dnac._exec( family="software_image_management_swim", @@ -638,6 +740,18 @@ def get_diff_tagging(self): self.result['response'] = task_details if task_details else response def get_diff_distribution(self): + """ + Get image distribution parameters from the playbook and trigger image distribution. + Args: + None + Returns: + None + Description: + This function retrieves image distribution parameters from the playbook's 'distribution_details' and triggers + the distribution of the specified software image to the specified device. It monitors the distribution task's + progress and updates the 'result' dictionary. If the operation is successful, 'changed' is set to True. + """ + distribution_details = self.want.get("distribution_details") distribution_params = dict( payload=[dict( @@ -645,8 +759,7 @@ def get_diff_distribution(self): imageUuid=self.have.get("distribution_image_id") )] ) - if self.log: - log("Distribution Params: " + str(distribution_params)) + log("Distribution Params: " + str(distribution_params)) response = self.dnac._exec( family="software_image_management_swim", @@ -672,6 +785,18 @@ def get_diff_distribution(self): self.result['response'] = task_details if task_details else response def get_diff_activation(self): + """ + Get image activation parameters from the playbook and trigger image activation. + Args: + None + Returns: + None + Description: + This function retrieves image activation parameters from the playbook's 'activation_details' and triggers the + activation of the specified software image on the specified device. It monitors the activation task's progress and + updates the 'result' dictionary. If the operation is successful, 'changed' is set to True. + """ + activation_details = self.want.get("activation_details") payload = [dict( activateLowerImageVersion=activation_details.get("activateLowerImageVersion"), @@ -684,8 +809,7 @@ def get_diff_activation(self): schedule_validate=activation_details.get("scehduleValidate"), payload=payload ) - if self.log: - log("Activation Params: " + str(activation_params)) + log("Activation Params: " + str(activation_params)) response = self.dnac._exec( family="software_image_management_swim", @@ -709,43 +833,67 @@ def get_diff_activation(self): self.result['response'] = task_details if task_details else response - def get_diff(self): - if self.want.get("tagging_details"): + def get_diff_merged(self, config): + """ + Get tagging details and then trigger distribution followed by activation if specified in the playbook. + Args: + config (dict): The configuration dictionary containing tagging, distribution, and activation details. + Returns: + self: The current instance of the class with updated 'result' and 'have' attributes. + Description: + This function checks the provided playbook configuration for tagging, distribution, and activation details. It + then triggers these operations in sequence if the corresponding details are found in the configuration.The + function monitors the progress of each task and updates the 'result' dictionary accordingly. If any of the + operations are successful, 'changed' is set to True. + """ + + if config.get("taggingDetails"): self.get_diff_tagging() - if self.want.get("distribution_details"): + if config.get("imageDistributionDetails"): self.get_diff_distribution() - if self.want.get("activation_details"): + if config.get("imageActivationDetails"): self.get_diff_activation() + return self + def main(): """ main entry point for module execution """ - element_spec = dict( - dnac_host=dict(required=True, type='str'), - dnac_port=dict(type='str', default='443'), - dnac_username=dict(type='str', default='admin', aliases=["user"]), - dnac_password=dict(type='str', no_log=True), - dnac_verify=dict(type='bool', default='True'), - dnac_version=dict(type="str", default="2.2.3.3"), - dnac_debug=dict(type='bool', default=False), - dnac_log=dict(type='bool', default=False), - config=dict(required=True, type='list', elements='dict'), - validate_response_schema=dict(type="bool", default=True), - ) + element_spec = {'dnac_host': {'required': True, 'type': 'str'}, + 'dnac_port': {'type': 'str', 'default': '443'}, + 'dnac_username': {'type': 'str', 'default': 'admin', 'aliases': ['user']}, + 'dnac_password': {'type': 'str', 'no_log': True}, + 'dnac_verify': {'type': 'bool', 'default': 'True'}, + 'dnac_version': {'type': 'str', 'default': '2.2.3.3'}, + 'dnac_debug': {'type': 'bool', 'default': False}, + 'dnac_log': {'type': 'bool', 'default': False}, + 'validate_response_schema': {'type': 'bool', 'default': True}, + 'config': {'required': True, 'type': 'list', 'elements': 'dict'}, + 'state': {'default': 'merged', 'choices': ['merged']} + } module = AnsibleModule(argument_spec=element_spec, supports_check_mode=False) dnac_swims = DnacSwims(module) - dnac_swims.validate_input() - dnac_swims.get_want() - dnac_swims.get_diff_import() - dnac_swims.get_have() - dnac_swims.get_diff() + state = dnac_swims.params.get("state") + + if state not in dnac_swims.supported_states: + dnac_swims.status = "invalid" + dnac_swims.msg = "State {0} is invalid".format(state) + dnac_swims.check_return_status() + + dnac_swims.validate_input().check_return_status() + for config in dnac_swims.validated_config: + dnac_swims.reset_values() + dnac_swims.get_want(config).check_return_status() + dnac_swims.get_diff_import().check_return_status() + dnac_swims.get_have().check_return_status() + dnac_swims.get_diff_state_apply[state](config).check_return_status() module.exit_json(**dnac_swims.result) diff --git a/plugins/modules/template_intent.py b/plugins/modules/template_intent.py index d713eae46b..69e850ddc3 100644 --- a/plugins/modules/template_intent.py +++ b/plugins/modules/template_intent.py @@ -666,10 +666,12 @@ class DnacTemplate(DnacBase): """Class containing member attributes for template intent module""" + def __init__(self, module): super().__init__(module) - self.have_create_project = {} - self.have_create_template = {} + self.have_project = {} + self.have_template = {} + self.supported_states = ["merged", "deleted"] self.accepted_languages = ["JINJA", "VELOCITY"] def validate_input(self): @@ -688,6 +690,7 @@ def validate_input(self): 'customParamsOrder': {'type': 'bool'}, 'description': {'type': 'str'}, 'deviceTypes': {'type': 'list', 'elements': 'dict'}, + # 'deviceTypes': {'type': 'list', 'productFamily': {'type': 'list', 'elements': 'dict'}}, 'failurePolicy': {'type': 'str'}, 'id': {'type': 'str'}, 'language': {'type': 'str'}, @@ -787,7 +790,7 @@ def get_template(self, config): def get_have_project(self, config): """Get the current project related information from DNAC""" - have_create_project = {} + have_project = {} given_project_name = config.get("projectName") template_available = None @@ -805,11 +808,11 @@ def get_have_project(self, config): return None template_available = project_details[0].get('templates') - have_create_project["project_found"] = True - have_create_project["id"] = project_details[0].get("id") - have_create_project["isDeletable"] = project_details[0].get("isDeletable") + have_project["project_found"] = True + have_project["id"] = project_details[0].get("id") + have_project["isDeletable"] = project_details[0].get("isDeletable") - self.have_create_project = have_create_project + self.have_project = have_project return template_available def get_have_template(self, config, template_available): @@ -818,10 +821,10 @@ def get_have_template(self, config, template_available): project_name = config.get("projectName") template_name = config.get("templateName") template = None - have_create_template = {} + have_template = {} - have_create_template["isCommitPending"] = False - have_create_template["template_found"] = False + have_template["isCommitPending"] = False + have_template["template_found"] = False template_details = get_dict_result(template_available, "name", @@ -834,14 +837,14 @@ def get_have_template(self, config, template_available): return self config["templateId"] = template_details.get("id") - have_create_template["id"] = template_details.get("id") + have_template["id"] = template_details.get("id") # Get available templates which are committed under the project template_list = self.dnac_apply['exec']( family="configuration_templates", function="gets_the_templates_available", params={"project_names": config.get("projectName")}, ) - have_create_template["isCommitPending"] = True + have_template["isCommitPending"] = True # This check will fail if specified template is there not committed in dnac if template_list and isinstance(template_list, list): template_info = get_dict_result(template_list, @@ -849,9 +852,9 @@ def get_have_template(self, config, template_available): template_name) if template_info: template = self.get_template(config) - have_create_template["template"] = template - have_create_template["isCommitPending"] = False - have_create_template["template_found"] = template is not None \ + have_template["template"] = template + have_template["isCommitPending"] = False + have_template["template_found"] = template is not None \ and isinstance(template, dict) self.log("Template {0} is found and template " "details are :{1}".format(template_name, str(template))) @@ -859,9 +862,9 @@ def get_have_template(self, config, template_available): # There are committed templates in the project but the # one specified in the playbook may not be committed self.log("Commit pending for template name {0}" - " is {1}".format(template_name, have_create_template.get('isCommitPending'))) + " is {1}".format(template_name, have_template.get('isCommitPending'))) - self.have_create_template = have_create_template + self.have_template = have_template self.msg = "Successfully collected all template parameters from dnac for comparison" self.status = "success" return self @@ -893,7 +896,7 @@ def get_want(self, config): """Get all the template and project related information from playbook that is needed to be created in DNAC""" - want_create = {} + want = {} template_params = self.get_template_params(config) project_params = self.get_project_params(config) version_comments = config.get("versionDescription") @@ -901,11 +904,11 @@ def get_want(self, config): if self.params.get("state") == "merged": self.update_mandatory_parameters(template_params) - want_create["template_params"] = template_params - want_create["project_params"] = project_params - want_create["comments"] = version_comments + want["template_params"] = template_params + want["project_params"] = project_params + want["comments"] = version_comments - self.want_create = want_create + self.want = want self.msg = "Successfully collected all parameters from playbook " + \ "for comparison" self.status = "success" @@ -916,8 +919,8 @@ def create_project_or_template(self, is_create_project=False): creation_id = None created = False - template_params = self.want_create.get("template_params") - project_params = self.want_create.get("project_params") + template_params = self.want.get("template_params") + project_params = self.want.get("project_params") if is_create_project: params_key = project_params @@ -979,12 +982,12 @@ def create_project_or_template(self, is_create_project=False): def requires_update(self): """Check if the template config given requires update.""" - if self.have_create_template.get("isCommitPending"): + if self.have_template.get("isCommitPending"): self.log("Template is in saved state and needs to be updated and committed") return True - current_obj = self.have_create_template.get("template") - requested_obj = self.want_create.get("template_params") + current_obj = self.have_template.get("template") + requested_obj = self.want.get("template_params") obj_params = [ ("tags", "tags", ""), ("author", "author", ""), @@ -1023,17 +1026,17 @@ def update_mandatory_parameters(self, template_params): # Mandate fields required for creating a new template. # Store it with other template parameters. - template_params["projectId"] = self.have_create_project.get("id") - template_params["project_id"] = self.have_create_project.get("id") + template_params["projectId"] = self.have_project.get("id") + template_params["project_id"] = self.have_project.get("id") # Update language,deviceTypes and softwareType if not provided for existing template. if not template_params.get("language"): - template_params["language"] = self.have_create_template.get('template') \ + template_params["language"] = self.have_template.get('template') \ .get('language') if not template_params.get("deviceTypes"): - template_params["deviceTypes"] = self.have_create_template.get('template') \ + template_params["deviceTypes"] = self.have_template.get('template') \ .get('deviceTypes') if not template_params.get("softwareType"): - template_params["softwareType"] = self.have_create_template.get('template') \ + template_params["softwareType"] = self.have_template.get('template') \ .get('softwareType') def validate_input_merge(self, template_exists): @@ -1043,7 +1046,7 @@ def validate_input_merge(self, template_exists): "It is not required to be provided in playbook, " "but if it is new creation error will be thrown to provide these fields.""" - template_params = self.want_create.get("template_params") + template_params = self.want.get("template_params") language = template_params.get("language").upper() if language: if language not in self.accepted_languages: @@ -1069,7 +1072,7 @@ def validate_input_merge(self, template_exists): def get_diff_merged(self, config): """Update/Create templates and projects in DNAC with fields provided in DNAC""" - is_project_found = self.have_create_project.get("project_found") + is_project_found = self.have_project.get("project_found") if not is_project_found: project_id, project_created = self.create_project_or_template(is_create_project=True) if project_created: @@ -1079,8 +1082,8 @@ def get_diff_merged(self, config): self.msg = "Project creation failed" return self - is_template_found = self.have_create_template.get("template_found") - template_params = self.want_create.get("template_params") + is_template_found = self.have_template.get("template_found") + template_params = self.want.get("template_params") template_id = None template_updated = False self.validate_input_merge(is_template_found).check_return_status() @@ -1093,11 +1096,11 @@ def get_diff_merged(self, config): op_modifies=True, ) template_updated = True - template_id = self.have_create_template.get("id") + template_id = self.have_template.get("id") self.log("Updating Existing Template") else: # Template does not need update - self.result['response'] = self.have_create_template.get("template") + self.result['response'] = self.have_template.get("template") self.result['msg'] = "Template does not need update" self.status = "exited" return self @@ -1112,7 +1115,7 @@ def get_diff_merged(self, config): if template_updated: # Template needs to be versioned version_params = { - "comments": self.want_create.get("comments"), + "comments": self.want.get("comments"), "templateId": template_id } response = self.dnac_apply['exec']( @@ -1147,12 +1150,12 @@ def delete_project_or_template(self, config, is_delete_project=False): """Call DNAC API to delete project or template with provided inputs""" if is_delete_project: - params_key = {"project_id": self.have_create_project.get("id")} + params_key = {"project_id": self.have_project.get("id")} deletion_value = "deletes_the_project" name = "project: {0}".format(config.get('projectName')) else: - template_params = self.want_create.get("template_params") - params_key = {"template_id": self.have_create_template.get("id")} + template_params = self.want.get("template_params") + params_key = {"template_id": self.have_template.get("id")} deletion_value = "deletes_the_template" name = "templateName: {0}".format(template_params.get('templateName')) @@ -1182,7 +1185,7 @@ def delete_project_or_template(self, config, is_delete_project=False): def get_diff_deleted(self, config): """Delete projects or templates in DNAC with fields provided in playbook.""" - is_project_found = self.have_create_project.get("project_found") + is_project_found = self.have_project.get("project_found") projectName = config.get("projectName") if not is_project_found: @@ -1190,8 +1193,8 @@ def get_diff_deleted(self, config): self.status = "failed" return self - is_template_found = self.have_create_template.get("template_found") - template_params = self.want_create.get("template_params") + is_template_found = self.have_template.get("template_found") + template_params = self.want.get("template_params") template_name = config.get("templateName") if template_params.get("name"): if is_template_found: @@ -1202,7 +1205,7 @@ def get_diff_deleted(self, config): return self else: self.log("Template Name is empty, deleting the project and its associated templates") - is_project_deletable = self.have_create_project.get("isDeletable") + is_project_deletable = self.have_project.get("isDeletable") if is_project_deletable: self.delete_project_or_template(config, is_delete_project=True) else: @@ -1217,9 +1220,9 @@ def get_diff_deleted(self, config): def reset_values(self): """Reset all neccessary attributes to default values""" - self.have_create_project.clear() - self.have_create_template.clear() - self.want_create.clear() + self.have_project.clear() + self.have_template.clear() + self.want.clear() def main(): diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index 4cd6816af6..91360148ff 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -352,6 +352,7 @@ plugins/action/syslog_config_create.py compile-2.6!skip # Python 2.6 is not supp plugins/action/syslog_config_update.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/action/transit_peer_network.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/action/transit_peer_network_info.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/template_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/site_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK @@ -706,16 +707,19 @@ plugins/action/syslog_config_create.py compile-2.7!skip # Python 2.7 is not supp plugins/action/syslog_config_update.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/action/transit_peer_network.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/action/transit_peer_network_info.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/template_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/site_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/swim_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/module_utils/dnac.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK 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/module_utils/dnac.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK 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 diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index c18cef2393..36f702f1b3 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -354,6 +354,7 @@ plugins/action/syslog_config_create.py compile-2.6!skip # Python 2.6 is not supp plugins/action/syslog_config_update.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/action/transit_peer_network.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/action/transit_peer_network_info.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/template_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/site_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK @@ -708,6 +709,7 @@ plugins/action/syslog_config_create.py compile-2.7!skip # Python 2.7 is not supp plugins/action/syslog_config_update.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/action/transit_peer_network.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/action/transit_peer_network_info.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/template_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/site_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK @@ -1062,11 +1064,13 @@ plugins/action/syslog_config_create.py import-2.7 # Python 2.7 is not supported plugins/action/syslog_config_update.py import-2.7 # Python 2.7 is not supported by the DNA Center SDK plugins/action/transit_peer_network.py import-2.7 # Python 2.7 is not supported by the DNA Center SDK plugins/action/transit_peer_network_info.py import-2.7 # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK 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/module_utils/dnac.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK 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 diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt index 4c6b7ecdbb..a6ce8c2866 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -2,19 +2,23 @@ plugins/module_utils/dnac.py compile-2.6!skip # Python 2.6 is not supported by t plugins/module_utils/dnac.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/module_utils/dnac.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/module_utils/dnac.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/template_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/site_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/swim_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK 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/network_settings_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/template_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/site_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/swim_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK 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 \ No newline at end of file +plugins/modules/swim_intent.py import-2.7!skip # Python 2.7 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 1ef6913d16..a0cfbc0671 100644 --- a/tests/sanity/ignore-2.13.txt +++ b/tests/sanity/ignore-2.13.txt @@ -1,10 +1,12 @@ plugins/module_utils/dnac.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/module_utils/dnac.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/template_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/site_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/swim_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK 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 \ No newline at end of file +plugins/modules/swim_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 1ef6913d16..a0cfbc0671 100644 --- a/tests/sanity/ignore-2.14.txt +++ b/tests/sanity/ignore-2.14.txt @@ -1,10 +1,12 @@ plugins/module_utils/dnac.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/module_utils/dnac.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/template_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/site_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/swim_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK 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 \ No newline at end of file +plugins/modules/swim_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 1ef6913d16..a0cfbc0671 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -1,10 +1,12 @@ plugins/module_utils/dnac.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/module_utils/dnac.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/template_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/site_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/swim_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK 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 \ No newline at end of file +plugins/modules/swim_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 4cd6816af6..91360148ff 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -352,6 +352,7 @@ plugins/action/syslog_config_create.py compile-2.6!skip # Python 2.6 is not supp plugins/action/syslog_config_update.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/action/transit_peer_network.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/action/transit_peer_network_info.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/template_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/site_intent.py compile-2.6!skip # Python 2.6 is not supported by the DNA Center SDK @@ -706,16 +707,19 @@ plugins/action/syslog_config_create.py compile-2.7!skip # Python 2.7 is not supp plugins/action/syslog_config_update.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/action/transit_peer_network.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/action/transit_peer_network_info.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/template_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/site_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/swim_intent.py compile-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/module_utils/dnac.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py import-2.6!skip # Python 2.6 is not supported by the DNA Center SDK 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/module_utils/dnac.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK +plugins/modules/network_settings_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK plugins/modules/pnp_intent.py import-2.7!skip # Python 2.7 is not supported by the DNA Center SDK 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