From 0e1f11ae3f2adab5be5542b2d9a697d30059760f Mon Sep 17 00:00:00 2001 From: Mallik M J Date: Fri, 30 Aug 2024 15:28:40 +0530 Subject: [PATCH 1/6] DCNM Security Groups Associations commit after IT/UT complete --- README.md | 6 +- ...isco.dcnm.dcnm_sgrp_association_module.rst | 460 +++ plugins/module_utils/common/common_utils.py | 257 ++ .../dcnm/dcnm_sgrp_association_utils.py | 868 +++++ plugins/modules/dcnm_sgrp_association.py | 1150 ++++++ .../dcnm_sgrp_association/defaults/main.yaml | 2 + .../dcnm_sgrp_association/meta/main.yaml | 2 + .../dcnm_sgrp_association/tasks/dcnm.yaml | 24 + .../dcnm_sgrp_association/tasks/main.yaml | 2 + .../tests/dcnm/dcnm_sgrp_assoc_delete.yaml | 273 ++ .../tests/dcnm/dcnm_sgrp_assoc_merge.yaml | 232 ++ .../tests/dcnm/dcnm_sgrp_assoc_override.yaml | 213 ++ .../tests/dcnm/dcnm_sgrp_assoc_query.yaml | 229 ++ .../tests/dcnm/dcnm_sgrp_assoc_replace.yaml | 135 + tests/sanity/ignore-2.10.txt | 1 + tests/sanity/ignore-2.11.txt | 1 + tests/sanity/ignore-2.12.txt | 1 + tests/sanity/ignore-2.13.txt | 1 + tests/sanity/ignore-2.14.txt | 1 + tests/sanity/ignore-2.15.txt | 1 + tests/sanity/ignore-2.16.txt | 1 + tests/sanity/ignore-2.9.txt | 1 + .../fixtures/common/common_responses.json | 129 + .../dcnm_sgrp_association_common.py | 56 + .../dcnm_sgrp_association_config.json | 56 + .../dcnm_sgrp_association_data.json | 126 + .../dcnm_sgrp_association_response.json | 57 + .../dcnm/test_dcnm_sgrp_associtaion.py | 3140 +++++++++++++++++ 28 files changed, 7424 insertions(+), 1 deletion(-) create mode 100644 docs/cisco.dcnm.dcnm_sgrp_association_module.rst create mode 100644 plugins/module_utils/common/common_utils.py create mode 100644 plugins/module_utils/network/dcnm/dcnm_sgrp_association_utils.py create mode 100644 plugins/modules/dcnm_sgrp_association.py create mode 100644 tests/integration/targets/dcnm_sgrp_association/defaults/main.yaml create mode 100644 tests/integration/targets/dcnm_sgrp_association/meta/main.yaml create mode 100644 tests/integration/targets/dcnm_sgrp_association/tasks/dcnm.yaml create mode 100644 tests/integration/targets/dcnm_sgrp_association/tasks/main.yaml create mode 100644 tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_delete.yaml create mode 100644 tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_merge.yaml create mode 100644 tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_override.yaml create mode 100644 tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_query.yaml create mode 100644 tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_replace.yaml create mode 100644 tests/unit/modules/dcnm/fixtures/common/common_responses.json create mode 100644 tests/unit/modules/dcnm/fixtures/dcnm_sgrp_association/dcnm_sgrp_association_common.py create mode 100644 tests/unit/modules/dcnm/fixtures/dcnm_sgrp_association/dcnm_sgrp_association_config.json create mode 100644 tests/unit/modules/dcnm/fixtures/dcnm_sgrp_association/dcnm_sgrp_association_data.json create mode 100644 tests/unit/modules/dcnm/fixtures/dcnm_sgrp_association/dcnm_sgrp_association_response.json create mode 100644 tests/unit/modules/dcnm/test_dcnm_sgrp_associtaion.py diff --git a/README.md b/README.md index e631bd7e8..91aa04396 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,10 @@ This collection is intended for use with the following release versions: ## Ansible version compatibility -This collection has been tested against following Ansible versions: **>=2.9.10**. +This collection has been tested against following Ansible versions: **>=2.15.0**. +For collections that support Ansible 2.9, please ensure you update your `network_os` to use the +fully qualified collection name (for example, `cisco.ios.ios`). Plugins and modules within a collection may be tested with only specific Ansible versions. A collection may contain metadata that identifies these versions. PEP440 is the schema used to describe the versions of Ansible. @@ -39,6 +41,7 @@ Name | Description [cisco.dcnm.dcnm_interface](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_interface_module.rst)|DCNM Ansible Module for managing interfaces. [cisco.dcnm.dcnm_inventory](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_inventory_module.rst)|Add and remove Switches from a DCNM managed VXLAN fabric. [cisco.dcnm.dcnm_links](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_links_module.rst)|DCNM ansible module for managing Links. +[cisco.dcnm.dcnm_maintenance_mode](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_maintenance_mode_module.rst)|Manage Maintenance Mode Configuration of NX-OS Switches. [cisco.dcnm.dcnm_network](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_network_module.rst)|Add and remove Networks from a DCNM managed VXLAN fabric. [cisco.dcnm.dcnm_policy](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_policy_module.rst)|DCNM Ansible Module for managing policies. [cisco.dcnm.dcnm_resource_manager](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_resource_manager_module.rst)|DCNM ansible module for managing resources. @@ -46,6 +49,7 @@ Name | Description [cisco.dcnm.dcnm_service_node](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_service_node_module.rst)|Create/Modify/Delete service node based on type and attached interfaces from a DCNM managed VXLAN fabric. [cisco.dcnm.dcnm_service_policy](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_service_policy_module.rst)|DCNM ansible module for managing service policies. [cisco.dcnm.dcnm_service_route_peering](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_service_route_peering_module.rst)|DCNM Ansible Module for managing Service Route Peerings. +[cisco.dcnm.dcnm_sgrp_association](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_sgrp_association_module.rst)|DCNM Ansible Module for managing Security Groups Associatons. [cisco.dcnm.dcnm_template](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_template_module.rst)|DCNM Ansible Module for managing templates. [cisco.dcnm.dcnm_vpc_pair](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_vpc_pair_module.rst)|DCNM Ansible Module for managing VPC switch pairs required for VPC interfaces. [cisco.dcnm.dcnm_vrf](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_vrf_module.rst)|Add and remove VRFs from a DCNM managed VXLAN fabric. diff --git a/docs/cisco.dcnm.dcnm_sgrp_association_module.rst b/docs/cisco.dcnm.dcnm_sgrp_association_module.rst new file mode 100644 index 000000000..eacd3c35e --- /dev/null +++ b/docs/cisco.dcnm.dcnm_sgrp_association_module.rst @@ -0,0 +1,460 @@ +.. _cisco.dcnm.dcnm_sgrp_association_module: + + +******************************** +cisco.dcnm.dcnm_sgrp_association +******************************** + +**DCNM Ansible Module for managing Security Groups Associatons.** + + +Version added: 3.5.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- DCNM Ansible Module for managing Security Groups Associations. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ config + +
+ list + / elements=dictionary +
+
+ Default:
[]
+
+
A list of dictionaries containing Security Group information
+
+
+ contract_name + +
+ string + / required +
+
+ +
Contract name associated with the Security Group Association.
+
+
+ dst_group_id + +
+ integer +
+
+ +
A unique identifier to identify the destination group. This argument is optional and will be allocated by the module before a payload is pushed to the controller. If this argument is included in the input, then the user provided argument is used.
+
This argument takes a minimum value of 16 and a maximum value of 65535.
+
+
+ dst_group_name + +
+ string + / required +
+
+ +
Name of the destination Security Group in the association.
+
This argument must have a minimum length of 1 and a maximum length of 63.
+
+
+ src_group_id + +
+ integer +
+
+ +
A unique identifier to identify the source group. This argument is optional and will be allocated by the module before a payload is pushed to the controller. If this argument is included in the input, then the user provided argument is used.
+
This argument takes a minimum value of 16 and a maximum value of 65535.
+
+
+ src_group_name + +
+ string + / required +
+
+ +
Name of the source Security Group in the association.
+
This argument must have a minimum length of 1 and a maximum length of 63.
+
+
+ switch + +
+ list + / elements=string + / required +
+
+ +
IP address or DNS name of the management interface. All switches mentioned in this list will be deployed with the included configuration.
+
+
+ vrf_name + +
+ string + / required +
+
+ +
VRF name associated with the Security Group Association.
+
This argument must have a minimum length of 1 and a maximum length of 32.
+
+
+ deploy + +
+ string +
+
+
    Choices: +
  • none
  • +
  • switches ←
  • +
+
+
Flag indicating if the configuration must be pushed to the switch.
+
A value of 'none' will not push the changes to the controller. A value of 'switches' will perform switch level deploy for the changes made.
+
+
+ fabric + +
+ string + / required +
+
+ +
Name of the target fabric for Security Group Association operations
+
+
+ state + +
+ string +
+
+
    Choices: +
  • merged ←
  • +
  • replaced
  • +
  • overridden
  • +
  • deleted
  • +
  • query
  • +
+
+
The required state of the configuration after module completion.
+
+
+ + + + +Examples +-------- + +.. code-block:: yaml + + # States: + # This module supports the following states: + # + # Merged: + # Security Group Associations defined in the playbook will be merged into the target fabric. + # + # The Security Group Associations listed in the playbook will be created if not already present on the DCNM + # server. If the Security Group Association is already present and the configuration information included + # in the playbook is either different or not present in DCNM, then the corresponding + # information is added to the DCNM. If a Security Group Asssociation mentioned in playbook + # is already present on DCNM and there is no difference in configuration, no operation + # will be performed for such groups. + # + # Replaced: + # Security Group Associations defined in the playbook will be replaced in the target fabric. + # + # The state of the Security Group Associations listed in the playbook will serve as source of truth for the + # same Security Group Associations present on the DCNM under the fabric mentioned. Additions and updations + # will be done to bring the DCNM Security Group Associations to the state listed in the playbook. + # Note: Replace will only work on the Security Group Associations mentioned in the playbook. + # + # Overridden: + # Security Group Associations defined in the playbook will be overridden in the target fabric. + # + # The state of the Security Group Associations listed in the playbook will serve as source of truth for all + # the Security Group Associations under the fabric mentioned. Additions and deletions will be done to bring + # the DCNM Security Group Associations to the state listed in the playbook. All Security Group Associations other than the + # ones mentioned in the playbook will be deleted. + # Note: Override will work on the all the Security Group Associations present in the DCNM Fabric. + # + # Deleted: + # Security Group Associations defined in the playbook will be deleted in the target fabric. + # + # Deletes the list of Security Group Associations specified in the playbook. If the playbook does not include + # any Security Group Association information, then all Security Group Associations from the fabric will be deleted. + # + # Query: + # Returns the current DCNM state for the Security Group Associations listed in the playbook. + + # CREATE SECURITY GROUP ASSOCIATIONS + + - name: Create Security Group Associations - with and without mentioning group IDs + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + deploy: switches # choose from ["none", "switches"] + state: merged # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15001" + dst_group_name: "LSG_15001" + src_group_id: 15001 # Group Id associated with src_group_name + dst_group_id: 15001 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50001" + contract_name: CONTRACT1 + switch: + - 192.168.1.1 + - 192.168.1.2 + + - src_group_name: "LSG_15002" + dst_group_name: "LSG_15002" + vrf_name: "MyVRF_50002" + contract_name: CONTRACT1 + switch: + - 192.168.1.1 + - 192.168.1.2 + + # DELETE SECURITY GROUP ASSOCIATIONS + + - name: Delete Security Group Associations - without config + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + + - name: Delete Security Group Associations - with group name + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + config: + - src_group_name: "LSG_15001" + switch: + - 192.168.1.1 + + - name: Delete Security Group Associations - with group Id + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + config: + - dst_group_id: 15001 + + - name: Delete Security Group Associations - with vrf name + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + config: + - vrf_name: "MyVRF_50003" + switch: + - 192.168.1.2 + + - name: Delete Security Group Associations - with contract name + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + config: + - contract_name: "CONTRACT1" + + - name: Delete Security Group Associations - sepcifying all + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + config: + - src_group_id: 15001 + dst_group_id: 15002 + src_group_name: "LSG_15001" + dst_group_name: "LSG_15002" + vrf_name: "MyVRF_50003" + contract_name: "CONTRACT1" + + # REPLACE SECURITY GROUP ASSOCIATIONS + + - name: Replace Security Group Associations + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + deploy: switches # choose from ["none", "switches"] + state: replaced # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15001" + dst_group_name: "LSG_15001" + src_group_id: 15001 # Group Id associated with src_group_name + dst_group_id: 15001 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50001" + contract_name: ICMP-PERMIT + switch: + - 192.168.1.1 + - 192.168.1.2 + + # OVERRIDE SECURITY GROUP ASSOCIATIONS + + - name: Override Security Group Association without no config + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + deploy: switches # choose from ["none", "switches"] + state: overridden # choose from [merged, replaced, deleted, overridden, query] + + - name: Override Security Group Association with config + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + deploy: switches # choose from ["none", "switches"] + state: overridden # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15003" + dst_group_name: "LSG_15004" + src_group_id: 15003 # Group Id associated with src_group_name + dst_group_id: 15004 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50003" + contract_name: CONTRACT1 + switch: + - 192.168.1.1 + - 192.168.1.2 + + # QUERY SECURITY GROUP ASSOCIATIONS + + - name: Query Security Groups - without filters + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + deploy: none + state: query + + - name: Query Security Groups - with destination group name + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + deploy: none + state: query + config: + - dst_group_name: "LSG_15002" + + - name: Query Security Groups - with vrf name + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + deploy: none + state: query + config: + - vrf_name: "MyVRF_50003" + + - name: Query Security Groups - with group id + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + deploy: none + state: query + config: + - src_group_id: 15001 + + - name: Query Security Groups - with contract name + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + deploy: none + state: query + config: + - contract_name: CONTRACT1 + + + + +Status +------ + + +Authors +~~~~~~~ + +- Mallik Mudigonda(@mmudigon) diff --git a/plugins/module_utils/common/common_utils.py b/plugins/module_utils/common/common_utils.py new file mode 100644 index 000000000..155eefcd5 --- /dev/null +++ b/plugins/module_utils/common/common_utils.py @@ -0,0 +1,257 @@ +from __future__ import absolute_import, division, print_function + +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + get_fabric_inventory_details, + dcnm_version_supported, + get_ip_sn_dict, +) + +__metaclass__ = type +__author__ = "Mallik Mudigonda" + + +class Version: + def __init__(self): + self.class_name = self.__class__.__name__ + self._module = None + + @property + def module(self): + return self._module + + @module.setter + def module(self, value): + self._module = value + + def commit(self): + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + if self._module is None: + msg = f"{0}.commit(): ".format(self.class_name) + msg += "module is not set, which is required." + raise ValueError(msg) + + self.dcnm_version = dcnm_version_supported(self._module) + self.log.debug(f"Version = {0}\n".format(self.dcnm_version)) + + +class InventoryData: + def __init__(self): + + self.class_name = self.__class__.__name__ + self.inventory_data = None + self._module = None + self._fabric = None + + @property + def module(self): + return self._module + + @module.setter + def module(self, value): + self._module = value + + @property + def fabric(self): + return self._fabric + + @fabric.setter + def fabric(self, value): + self._fabric = value + + def commit(self): + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + if self._module is None: + msg = f"{0}.commit(): ".format(self.class_name) + msg += "module is not set, which is required." + raise ValueError(msg) + + if self._fabric is None: + msg = f"{0}.commit(): ".format(self.class_name) + msg += "fabric is not set, which is required." + raise ValueError(msg) + + self.inventory_data = get_fabric_inventory_details( + self._module, self._fabric + ) + + inv_data = [ + { + "IP": d["ipAddress"], + "Sno": d["serialNumber"], + "Logical Name": d["logicalName"], + "Managable": d["managable"], + "Role": d["switchRoleEnum"], + } + for d in self.inventory_data.values() + ] + self.log.debug(f"Inventory Data = {0}\n".format(inv_data)) + + +class FabricInfo: + def __init__(self): + self.class_name = self.__class__.__name__ + self.monitoring = [] + self._module = None + self._fabric = None + self._rest_send = None + self._paths = None + + @property + def module(self): + return self._module + + @module.setter + def module(self, value): + self._module = value + + @property + def fabric(self): + return self._fabric + + @fabric.setter + def fabric(self, value): + self._fabric = value + + @property + def paths(self): + return self._paths + + @paths.setter + def paths(self, value): + self._paths = value + + @property + def rest_send(self): + return self._rest_send + + @rest_send.setter + def rest_send(self, value): + self._rest_send = value + + def commit(self): + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + if self._module is None: + msg = f"{0}.commit(): ".format(self.class_name) + msg += "module is not set, which is required." + raise ValueError(msg) + + if self._fabric is None: + msg = f"{0}.commit(): ".format(self.class_name) + msg += "fabric is not set, which is required." + raise ValueError(msg) + + if self._rest_send is None: + msg = f"{0}.commit(): ".format(self.class_name) + msg += "rest_send is not set, which is required." + raise ValueError(msg) + + if self._paths is None: + msg = f"{0}.commit(): ".format(self.class_name) + msg += "paths is not set, which is required." + raise ValueError(msg) + + processed_fabrics = [] + processed_fabrics.append(self._fabric) + + # Get all fabrics which are in monitoring mode. Deploy must be avoided to all fabrics which are part of this list + for fabric in processed_fabrics: + + path = self._paths["FABRIC_ACCESS_MODE"].format(fabric) + + self._rest_send.path = path + self._rest_send.verb = "GET" + self._rest_send.payload = None + self._rest_send.commit() + + resp = self._rest_send.response[0] + + self.log.debug(f"RST: Fabric Access Mode Resp = {0}\n".format(resp)) + + if resp and resp["RETURN_CODE"] == 200: + if str(resp["DATA"]["readonly"]).lower() == "true": + self.monitoring.append(fabric) + + # Check if source fabric is in monitoring mode. If so return an error, since fabrics in monitoring mode do not allow + # create/modify/delete and deploy operations. + if self._fabric in self.monitoring: + self._module.fail_json( + msg="Error: Source Fabric '{0}' is in Monitoring mode, No changes are allowed on the fabric\n".format( + self._fabric + ) + ) + + +class SwitchInfo: + def __init__(self): + self.class_name = self.__class__.__name__ + self._inventory_data = None + + @property + def inventory_data(self): + return self._inventory_data + + @inventory_data.setter + def inventory_data(self, value): + self._inventory_data = value + + def commit(self): + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + if self._inventory_data is None: + msg = f"{0}.commit(): ".format(self.class_name) + msg += "inventory data is not set, which is required." + raise ValueError(msg) + + self.dcnm_update_switch_mapping_info() + self.dcnm_update_switch_managability_info() + + def dcnm_update_switch_mapping_info(self): + + # Based on the updated inventory_data, update ip_sn, hn_sn and sn_hn objects + self.ip_sn, self.hn_sn = get_ip_sn_dict(self.inventory_data) + self.sn_hn = dict([(value, key) for key, value in self.hn_sn.items()]) + self.sn_ip = dict([(value, key) for key, value in self.ip_sn.items()]) + + self.log.debug(f"IP_SN = {0}\n".format(self.ip_sn)) + self.log.debug(f"SN_HN = {0}\n".format(self.sn_hn)) + self.log.debug(f"SN_IP = {0}\n".format(self.sn_ip)) + + def dcnm_update_switch_managability_info(self): + + # Get all switches which are managable. Deploy must be avoided to all switches which are not part of this list + managable_ip = [ + (key, self.inventory_data[key]["serialNumber"]) + for key in self.inventory_data + if str(self.inventory_data[key]["managable"]).lower() == "true" + ] + managable_hosts = [ + ( + self.inventory_data[key]["logicalName"], + self.inventory_data[key]["serialNumber"], + ) + for key in self.inventory_data + if str(self.inventory_data[key]["managable"]).lower() == "true" + ] + self.managable = dict(managable_ip + managable_hosts) + + self.meta_switches = [ + ( + key, + self.inventory_data[key]["logicalName"], + self.inventory_data[key]["serialNumber"], + ) + for key in self.inventory_data + if self.inventory_data[key]["switchRoleEnum"] is None + ] + + self.log.debug(f"Meta Switches = {0}\n".format(self.meta_switches)) + self.log.debug(f"Managable Switches = {0}\n".format(self.managable)) diff --git a/plugins/module_utils/network/dcnm/dcnm_sgrp_association_utils.py b/plugins/module_utils/network/dcnm/dcnm_sgrp_association_utils.py new file mode 100644 index 000000000..8c5892173 --- /dev/null +++ b/plugins/module_utils/network/dcnm/dcnm_sgrp_association_utils.py @@ -0,0 +1,868 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import json +import time +import logging + +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + dcnm_send, +) + +# Some of the key names used in playbook is different from what is expected in payload. Translate such keys +xlate_key = { + "src_group_name": "srcGroupName", + "dst_group_name": "dstGroupName", + "src_group_id": "srcGroupId", + "dst_group_id": "dstGroupId", + "vrf_name": "vrfName", + "contract_name": "contractName", + "switch": "switch", + "fabric": "fabricName", +} + +rev_xlate_key = { + "srcGroupName": "src_group_name", + "dstGroupName": "dst_group_name", + "srcGroupId": "src_group_id", + "dstGroupId": "dst_group_id", + "vrfName": "vrf_name", + "contractName": "contract_name", + "switch": "switch", + "fabricName": "fabric", +} + +dcnm_sgrp_association_paths = { + 11: {}, + 12: { + "SGRP_GET_GROUP": "/appcenter/cisco/ndfc/api/v1/security/fabrics/{}/groups", + "SGRP_ASSOC_GET": "/appcenter/cisco/ndfc/api/v1/security/fabrics/{}/contractAssociations", + "SGRP_ASSOC_CREATE": "/appcenter/cisco/ndfc/api/v1/security/fabrics/{}/contractAssociations", + "SGRP_ASSOC_UPDATE": "/appcenter/cisco/ndfc/api/v1/security/fabrics/{}/contractAssociations", + "SGRP_ASSOC_DELETE": "/appcenter/cisco/ndfc/api/v1/security/fabrics/{}/contractAssociations/bulkDelete", + "SGRP_ASSOC_DEPLOY_BY_VRFS": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/vrfs/deploy", + "SGRP_ASSOC_DEPLOY_BY_SWITCHES": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{}/config-deploy/{}?forceShowRun=false", + "SGRP_ASSOC_GET_SYNC_STATUS": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{}/inventory/switchesByFabric", + "FABRIC_ACCESS_MODE": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{}/accessmode", + }, +} + + +class Paths: + def __init__(self): + self.class_name = self.__class__.__name__ + self._version = None + + @property + def version(self): + return self._version + + @version.setter + def version(self, value): + self._version = value + + def commit(self): + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + if self._version is None: + msg = f"{self.class_name}.commit(): " + msg += "version is not set, which is required." + raise ValueError(msg) + + self.paths = dcnm_sgrp_association_utils_get_paths(self._version) + self.log.debug(f"Paths = {0}\n".format(self.paths)) + + +def dcnm_sgrp_association_utils_get_all_sgrp_info(self): + + """ + Routine to get existing Security Groups information from DCNM. + + Parameters: + None + + Returns: + resp["DATA"] (dict): Security group information if available + [] otherwise + """ + + path = self.paths["SGRP_GET_GROUP"].format(self.fabric) + + resp = dcnm_send(self.module, "GET", path) + + self.log.info(f"DCNM:Get All Security Groups Resp = {resp}\n") + + if ( + resp + and (resp["RETURN_CODE"] == 200) + and (resp["MESSAGE"] == "OK") + and resp["DATA"] + ): + return resp["DATA"] + else: + return [] + + +def dcnm_sgrp_association_utils_get_paths(version): + return dcnm_sgrp_association_paths[version] + + +def dcnm_sgrp_association_utils_check_if_meta(self, dev): + + for elem in self.meta_switches: + if dev in elem: + return True + return False + + +def dcnm_sgrp_association_utils_validate_devices(self, cfg): + + for sw in cfg["switch"]: + if sw not in self.managable: + mesg = "Switch {0} is not Manageable".format(sw) + self.module.fail_json(msg=mesg) + + for sw in cfg["switch"]: + if dcnm_sgrp_association_utils_check_if_meta(self, sw): + mesg = "Switch {0} is not Manageable".format(sw) + self.module.fail_json(msg=mesg) + + +def dcnm_sgrp_association_utils_get_all_sgrp_association_info(self): + + """ + Routine to get existing Group Associations information from DCNM. + + Parameters: + None + + Returns: + resp["DATA"] (dict): Security group information if available + [] otherwise + """ + + # Get the Peer information first + path = self.paths["SGRP_ASSOC_GET"].format(self.fabric) + + resp = dcnm_send(self.module, "GET", path) + + self.log.debug(f"DCNM:Get All SGRP Associations Resp = {0}\n".format(resp)) + + if ( + resp + and (resp["RETURN_CODE"] == 200) + and (resp["MESSAGE"] == "OK") + and resp["DATA"] + ): + return resp["DATA"] + else: + return [] + + +def dcnm_sgrp_association_utils_translate_sgrp_association_info(self, elem): + + # Translate from playbook format to payload format + return { + "srcGroupName": elem.get("src_group_name", None), + "dstGroupName": elem.get("dst_group_name", None), + "srcGroupId": elem.get("src_group_id", None), + "dstGroupId": elem.get("dst_group_id", None), + "vrfName": elem.get("vrf_name", None), + "contractName": elem.get("contract_name", None), + } + + +def dcnm_sgrp_association_utils_get_sgrp_association_info(self, wobj): + + """ + Routine to get existing Security Group Association information from DCNM which matches the given object. + + Parameters: + wobj (dict): Object information in 'want' format + + Returns: + resp["DATA"] (dict): Security group association information obtained from the DCNM server if it exists + [] otherwise + """ + + if self.sgrp_association_list == []: + self.sgrp_association_list = dcnm_sgrp_association_utils_get_all_sgrp_association_info( + self + ) + + for sgrp_association in self.sgrp_association_list: + # Check if all the information match. Otherwise it is an error + if ( + ( + (wobj.get("srcGroupName") is None) + or (sgrp_association["srcGroupName"] == wobj["srcGroupName"]) + ) + and ( + (wobj.get("dstGroupName", None) is None) + or (sgrp_association["dstGroupName"] == wobj["dstGroupName"]) + ) + and ( + (wobj.get("srcGroupId", None) is None) + or (sgrp_association["srcGroupId"] == wobj["srcGroupId"]) + ) + and ( + (wobj.get("dstGroupId", None) is None) + or (sgrp_association["dstGroupId"] == wobj["dstGroupId"]) + ) + and ( + (wobj.get("vrfName", None) is None) + or (sgrp_association["vrfName"] == wobj["vrfName"]) + ) + ): + return sgrp_association + return [] + + +def dcnm_sgrp_association_utils_get_sgrp_association_payload( + self, sgrp_assoc_info +): + + sgrp_payload = {} + srcGroupId = "" + dstGroupId = "" + + if (sgrp_assoc_info.get("src_group_id", None) is None) or ( + sgrp_assoc_info.get("dst_group_id", None) is None + ): + if self.sgrp_info == {}: + sgrp_list = dcnm_sgrp_association_utils_get_all_sgrp_info(self) + [ + self.sgrp_info.update({sgrp["groupName"]: sgrp["groupId"]}) + for sgrp in sgrp_list + ] + + if ( + self.sgrp_info.get(sgrp_assoc_info["src_group_name"], None) + is not None + ): + srcGroupId = self.sgrp_info[sgrp_assoc_info["src_group_name"]] + + if ( + self.sgrp_info.get(sgrp_assoc_info["dst_group_name"], None) + is not None + ): + dstGroupId = self.sgrp_info[sgrp_assoc_info["dst_group_name"]] + else: + srcGroupId = sgrp_assoc_info["src_group_id"] + dstGroupId = sgrp_assoc_info["dst_group_id"] + + for key in sgrp_assoc_info: + sgrp_payload[xlate_key[key]] = sgrp_assoc_info[key] + sgrp_payload["fabricName"] = self.fabric + sgrp_payload["srcGroupId"] = srcGroupId + sgrp_payload["dstGroupId"] = dstGroupId + + return sgrp_payload + + +def dcnm_sgrp_association_utils_get_matching_want(self, sgrp_association_info): + + match_want = [ + want + for want in self.want + if ( + ( + (want.get("srcGroupName", "") == "") + or ( + sgrp_association_info["srcGroupName"] + == want["srcGroupName"] + ) + ) + and ( + (want.get("dstGroupName", "") == "") + or ( + sgrp_association_info["dstGroupName"] + == want["dstGroupName"] + ) + ) + and ( + (want.get("srcGroupId", "") == "") + or (sgrp_association_info["srcGroupId"] == want["srcGroupId"]) + ) + and ( + (want.get("dstGroupId", "") == "") + or (sgrp_association_info["dstGroupId"] == want["dstGroupId"]) + ) + and ( + (want.get("vrfName", "") == "") + or (sgrp_association_info["vrfName"] == want["vrfName"]) + ) + ) + ] + + return match_want + + +def dcnm_sgrp_association_utils_get_matching_have(self, want): + + match_have = [ + have + for have in self.have + if ( + ( + (want.get("srcGroupName", "") == "") + or (have["srcGroupName"] == want["srcGroupName"]) + ) + and ( + (want.get("dstGroupName", "") == "") + or (have["dstGroupName"] == want["dstGroupName"]) + ) + and ( + (want.get("srcGroupId", "") == "") + or (have["srcGroupId"] == want["srcGroupId"]) + ) + and ( + (want.get("dstGroupId", "") == "") + or (have["dstGroupId"] == want["dstGroupId"]) + ) + and ( + (want.get("vrfName", "") == "") + or (have["vrfName"] == want["vrfName"]) + ) + ) + ] + + return match_have + + +def dcnm_sgrp_association_utils_get_matching_cfg(self, want): + + match_cfg = [ + cfg + for cfg in self.config + if ( + ( + (cfg.get("src_group_name", "") == "") + or (want["srcGroupName"] == cfg["src_group_name"]) + ) + and ( + (cfg.get("dst_group_name", "") == "") + or (want["dstGroupName"] == cfg["dst_group_name"]) + ) + and ( + (cfg.get("src_group_id", "") == "") + or (want["srcGroupId"] == cfg["src_group_id"]) + ) + and ( + (cfg.get("dst_group_id", "") == "") + or (want["dstGroupId"] == cfg["dst_group_id"]) + ) + and ( + (cfg.get("vrf_name", "") == "") + or (want["vrfName"] == cfg["vrf_name"]) + ) + ) + ] + + return match_cfg + + +def dcnm_sgrp_association_utils_update_sgrp_association_information( + self, want, have, cfg +): + + for key in list(want.keys()): + if cfg.get(rev_xlate_key[key], None) is None: + # The given key from want is not included in the playbook config. Copy the + # information corresponding to the key from have + want[key] = have.get(key, "") + + +def dcnm_sgrp_association_utils_compare_sgrp_objects(self, wobj, hobj): + + """ + Routine to compare have and want objects and update mismatch information. + + Parameters: + wobj (dict): Requested object information + hobj (dict): Existing object information + Returns: + DCNM_SGRP_ASSOCIATION_EXIST(str): - if given Security Grouo Association is same as that exists + DCNM_SGRP_ASSOCIATION_MERGE(str): - if given Security Grouo Association exists but there are changes in parameters + mismatch_reasons(list): a list identifying objects that differed if required, [] otherwise + hobj(dict): existing object if required, [] otherwise + """ + + mismatch_reasons = [] + for key in wobj: + + if key == "switch": + continue + + if str(hobj.get(key, None)).lower() != str(wobj[key]).lower(): + # Differs in one of the params. + mismatch_reasons.append( + {key.upper() + "_MISMATCH": [wobj[key], hobj.get(key, None)]} + ) + + if mismatch_reasons != []: + return "DCNM_SGRP_ASSOCIATION_MERGE", mismatch_reasons, hobj + else: + return "DCNM_SGRP_ASSOCIATION_EXIST", [], [] + + +def dcnm_sgrp_association_utils_compare_want_and_have(self, want): + + """ + This routine finds an object in self.have that matches the given information. If the given + object already exist then it is not added to the object list to be created on + DCNM server in the current run. The given object is added to the list of objects to be + created otherwise. + + Parameters: + want : Object to be matched from self.have + + Returns: + DCNM_SGRP_ASSOCIATION_CREATE (str): - if a new object is to be created + return value of dcnm_sgrp_association_utils_compare_sgrp_objects + """ + + match_have = dcnm_sgrp_association_utils_get_matching_have(self, want) + + for melem in match_have: + return dcnm_sgrp_association_utils_compare_sgrp_objects( + self, want, melem + ) + + return "DCNM_SGRP_ASSOCIATION_CREATE", [], [] + + +def dcnm_sgrp_association_utils_get_delete_payload(self, elem): + + return elem["uuid"] + + +def dcnm_sgrp_association_utils_update_deploy_info(self, elem, deploy_object): + + # This routine updates the deploy list with a dictinary of key value pairs, where keys will be switch + # serial numbers, and the values will be a list of all VRFs associated with the given elem. This can be + # invoked for updating diff_deploy and diff_delete_deploy lists as well. + + # for delete state, switch is not a mandatory element. If switch is not included in the playbook, then apply + # it for all switches. + + if elem.get("switch", None) is None: + switch_list = self.ip_sn.keys() + else: + switch_list = elem["switch"] + + vrf_list = [] + for sw_elem in switch_list: + sno = self.ip_sn[sw_elem] + if deploy_object.get(sno, None) is None: + deploy_object[sno] = "" + + if elem["vrfName"] not in vrf_list: + vrf_list.append(elem["vrfName"]) + + if deploy_object[sno] != "": + cum_vrf_list = list(set(deploy_object[sno].split(",") + vrf_list)) + else: + cum_vrf_list = list(set(vrf_list)) + + deploy_object[sno] = ",".join(cum_vrf_list) + + +def dcnm_sgrp_association_utils_get_sgrp_association_deploy_payload( + self, elem, reason +): + + # Security Groups uses switch or vrf level deploy to deploy the changes. Each of this case requires + # a different payload. For switch level deploy case, we need the serial number of all switches. But for + # vrf level deploy, we will need a dict with switches as keys and VRFs as a list of values for each of these + # keys. + + if ( + (reason == "DCNM_SGRP_ASSOCIATION_CREATE") + or (reason == "DCNM_SGRP_ASSOCIATION_MERGE") + or ( + [ + sw_elem + for sw_elem in elem["switch"] + if self.sync_info.get(sw_elem, "Not-In-sync") != "In-Sync" + ] + != [] + ) + ): + dcnm_sgrp_association_utils_update_deploy_info( + self, elem, self.diff_deploy + ) + + +def dcnm_sgrp_association_utils_process_delete_payloads(self): + + """ + Routine to push delete payloads to DCNM server. This routine implements required error checks and retry mechanisms to handle + transient errors. + + Parameters: + None + + Returns: + None + """ + + resp = None + delete_flag = False + + if self.diff_delete: + path = self.paths["SGRP_ASSOC_DELETE"] + path = path.format(self.fabric) + + json_payload = json.dumps(self.diff_delete) + + resp = dcnm_send(self.module, "POST", path, json_payload) + + self.log.info( + f"DCNM:Delete Path = {path}, Resp = {resp}. Payload = {json_payload}\n" + ) + + if resp: + self.result["response"].append(resp) + + if resp and resp.get("RETURN_CODE") != 200: + resp["CHANGED"] = self.changed_dict[0] + self.module.fail_json(msg=resp) + else: + delete_flag = True + + if delete_flag: + dcnm_sgrp_association_utils_process_deploy_payloads( + self, self.diff_delete_deploy + ) + + return delete_flag + + +def dcnm_sgrp_association_utils_process_payloads_list( + self, payload_list, command, path +): + + """ + Routine to push payloads from the given list to DCNM server. This routine implements required error checks and retry mechanisms to handle + transient errors. + + Parameters: + None + + Returns: + None + """ + + resp = None + flag = False + + if command == "POST": + # Creates can happen in bulk. + json_payload = json.dumps(payload_list) + resp = dcnm_send(self.module, command, path, json_payload) + self.log.info( + f"DCNM:Create Path = {path}, Resp = {resp}, Payload - {json_payload}\n" + ) + if resp: + self.result["response"].append(resp) + if resp and resp.get("RETURN_CODE") != 200: + resp["CHANGED"] = self.changed_dict[0] + self.module.fail_json(msg=resp) + else: + flag = True + elif command == "PUT": + for elem in payload_list: + uuid = elem.pop("uuid") + json_payload = json.dumps(elem) + resp = dcnm_send( + self.module, command, path + "/" + str(uuid), json_payload + ) + self.log.info( + f"DCNM:Modify Path = {path}, Resp = {resp}, Payload = {json_payload}\n" + ) + + if resp: + self.result["response"].append(resp) + if resp and resp.get("RETURN_CODE") != 200: + resp["CHANGED"] = self.changed_dict[0] + self.module.fail_json(msg=resp) + else: + flag = True + return flag + + +def dcnm_sgrp_association_utils_process_create_payloads(self): + + """ + Routine to push create payloads to DCNM server. + + Parameters: + None + + Returns: + True if create payloads are successfully pushed to server + False otherwise + """ + + if self.diff_create == []: + return + + create_path = self.paths["SGRP_ASSOC_CREATE"].format(self.fabric) + + return dcnm_sgrp_association_utils_process_payloads_list( + self, self.diff_create, "POST", create_path + ) + + +def dcnm_sgrp_association_utils_process_modify_payloads(self): + + """ + Routine to push modify payloads to DCNM server. + + Parameters: + None + + Returns: + True if modified payloads are successfully pushed to server + False otherwise + """ + + if self.diff_modify == []: + return + + modify_path = self.paths["SGRP_ASSOC_UPDATE"].format(self.fabric) + + return dcnm_sgrp_association_utils_process_payloads_list( + self, self.diff_modify, "PUT", modify_path + ) + + +def dcnm_sgrp_association_utils_get_sync_status(self): + + """ + Routine to get switch status information for a given fabric. This information can be processed to get + the "In-Sync" status for the required switches. + transient errors. + + Parameters: + None + + Returns: + sync_status containing the SYNC status of all switches + """ + + resp = None + sync_info = {} + + path = self.paths["SGRP_ASSOC_GET_SYNC_STATUS"].format(self.fabric) + + resp = dcnm_send(self.module, "GET", path) + + self.log.info(f"DCNM:Get Switch SYNC Status Resp = {resp}\n") + status_info = [ + {f"({d['ipAddress']} : {d['ccStatus']}"} for d in resp["DATA"] + ] + self.log.info(f"Status Info = {status_info}\n") + + if resp and (resp["RETURN_CODE"] != 200): + resp["CHANGED"] = self.changed_dict[0] + self.module.fail_json(msg=resp) + else: + for elem in resp["DATA"]: + sync_info[elem["ipAddress"]] = elem["ccStatus"] + return sync_info + + +def dcnm_sgrp_association_utils_deploy_payload(self, deploy_info): + + """ + Routine to deploy a Security Group Associations to DCNM server. + + Parameters: + None + + Returns: + True if deploy succeded, False otherwise + """ + + resp = None + json_payload = None + deploy_flag = False + path = "" + + if self.deploy == "switches": + path = self.paths["SGRP_ASSOC_DEPLOY_BY_SWITCHES"].format( + self.fabric, ",".join(deploy_info.keys()) + ) + resp = dcnm_send(self.module, "POST", path) + # elif self.deploy == "vrfs": + # path = self.paths["SGRP_ASSOC_DEPLOY_BY_VRFS"] + # json_payload = json.dumps(deploy_info) + # resp = dcnm_send(self.module, "POST", path, json_payload) + + if resp: + self.result["response"].append(resp) + + self.log.info(f"DCNM:Deploy Element Path = {path}, Resp = {resp}\n") + self.log.info(f"Deploy Element Payload = {json_payload}\n") + + if resp and (resp["RETURN_CODE"] != 200): + resp["CHANGED"] = self.changed_dict[0] + self.module.fail_json(msg=resp) + else: + deploy_flag = True + + return deploy_flag, resp + + +def dcnm_sgrp_association_utils_process_deploy_payloads(self, deploy_info): + + """ + Routine to push deploy payloads to DCNM server. This routine implements required error checks and retry mechanisms to handle + transient errors. + + Parameters: + None + + Returns: + None + """ + + resp = None + deploy_flag = False + sync_info = {} + + if deploy_info == {}: + return deploy_flag + + rc, resp = dcnm_sgrp_association_utils_deploy_payload(self, deploy_info) + if deploy_flag is not True: + deploy_flag = rc + + if deploy_flag is True: + retries = 0 + while retries < 20: + + sync_info = dcnm_sgrp_association_utils_get_sync_status(self) + + if sync_info == {}: + continue + + deploy_keys = list(deploy_info.keys()) + for key in deploy_keys[:]: + if sync_info.get(self.sn_ip[key], "Not-In-Sync") == "In-Sync": + # Switch is In-Sync state. Remove that from deploy list. We will retry for other switches if any + deploy_info.pop(key) + + if deploy_info == {}: + break + else: + time.sleep(3) + retries += 1 + else: + resp["CHANGED"] = self.changed_dict[0] + self.module.fail_json( + msg=f"Switches {list(map(lambda x: self.sn_ip[x], deploy_info.keys()))} did not reach 'In-Sync' state after deploy\n" + ) + + if sync_info != {}: + sync_info["RETURN_CODE"] = 200 + sync_info["MESSAGE"] = "OK" + self.result["response"].append(sync_info) + + return deploy_flag + + +def dcnm_sgrp_association_utils_get_delete_list(self): + + del_list = [] + + if self.sgrp_association_list == []: + # Get all security group information present + sgrp_association_list = dcnm_sgrp_association_utils_get_all_sgrp_association_info( + self + ) + else: + sgrp_association_list = self.sgrp_association_list + + if sgrp_association_list == []: + return [] + + # If this info is not included in self.want, then go ahead and add it to del_list. Otherwise + # ignore this pair, since new configuration is included for this pair in the playbook. + for sgrp_association in sgrp_association_list: + want = dcnm_sgrp_association_utils_get_matching_want( + self, sgrp_association + ) + if want == []: + if sgrp_association not in del_list: + del_list.append(sgrp_association) + + return del_list + + +def dcnm_sgrp_association_utils_get_all_filtered_sgrp_association_objects( + self +): + + if self.sgrp_association_list == []: + sgrp_association_list = dcnm_sgrp_association_utils_get_all_sgrp_association_info( + self + ) + else: + sgrp_association_list = self.sgrp_association_list + + # If filters are provided, use the values to build the appropriate list. + if self.sgrp_association_info == []: + return sgrp_association_list + else: + sgrp_filtered_list = [] + + for elem in self.sgrp_association_info: + match = [ + sgrp_association + for sgrp_association in sgrp_association_list + if ( + ( + elem.get("src_group_id", None) is None + or sgrp_association["srcGroupId"] + == elem["src_group_id"] + ) + and ( + elem.get("src_group_name", None) is None + or sgrp_association["srcGroupName"] + == elem["src_group_name"] + ) + and ( + elem.get("dst_group_id", None) is None + or sgrp_association["dstGroupId"] + == elem["dst_group_id"] + ) + and ( + elem.get("dst_group_name", None) is None + or sgrp_association["dstGroupName"] + == elem["dst_group_name"] + ) + and ( + elem.get("vrf_name", None) is None + or sgrp_association["vrfName"] == elem["vrf_name"] + ) + and ( + elem.get("contract_name", None) is None + or sgrp_association["contractName"] + == elem["contract_name"] + ) + ) + ] + + if match == []: + continue + + for melem in match: + if melem not in sgrp_filtered_list: + sgrp_filtered_list.append(melem) + + return sgrp_filtered_list diff --git a/plugins/modules/dcnm_sgrp_association.py b/plugins/modules/dcnm_sgrp_association.py new file mode 100644 index 000000000..f8a198552 --- /dev/null +++ b/plugins/modules/dcnm_sgrp_association.py @@ -0,0 +1,1150 @@ +#!/usr/bin/python +# +# Copyright (c) 2020-2022 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type +__author__ = "Mallik Mudigonda" + +DOCUMENTATION = """ +--- +module: dcnm_sgrp_association +short_description: DCNM Ansible Module for managing Security Groups Associatons. +version_added: "3.5.0" +description: + - "DCNM Ansible Module for managing Security Groups Associations." +author: Mallik Mudigonda(@mmudigon) +options: + fabric: + description: + - Name of the target fabric for Security Group Association operations + type: str + required: true + state: + description: + - The required state of the configuration after module completion. + type: str + choices: ['merged', 'replaced', 'overridden', 'deleted', 'query'] + default: merged + deploy: + description: + - Flag indicating if the configuration must be pushed to the switch. + - A value of 'none' will not push the changes to the controller. A value + of 'switches' will perform switch level deploy for the changes made. + type: str + choices: ["none", "switches"] + default: switches + config: + description: + - A list of dictionaries containing Security Group information + type: list + elements: dict + default: [] + suboptions: + src_group_name: + description: + - Name of the source Security Group in the association. + - This argument must have a minimum length of 1 and a maximum length of 63. + required: true + type: str + src_group_id: + description: + - A unique identifier to identify the source group. This argument is optional and will be allocated by + the module before a payload is pushed to the controller. If this argument is included in the input, + then the user provided argument is used. + - This argument takes a minimum value of 16 and a maximum value of 65535. + type: int + dst_group_name: + description: + - Name of the destination Security Group in the association. + - This argument must have a minimum length of 1 and a maximum length of 63. + required: true + type: str + dst_group_id: + description: + - A unique identifier to identify the destination group. This argument is optional and will be allocated by + the module before a payload is pushed to the controller. If this argument is included in the input, + then the user provided argument is used. + - This argument takes a minimum value of 16 and a maximum value of 65535. + type: int + vrf_name: + description: + - VRF name associated with the Security Group Association. + - This argument must have a minimum length of 1 and a maximum length of 32. + type: str + required: true + contract_name: + description: + - Contract name associated with the Security Group Association. + type: str + required: true + switch: + description: + - IP address or DNS name of the management interface. All switches mentioned in this list + will be deployed with the included configuration. + type: list + elements: str + required: true +""" + +EXAMPLES = """ + +# States: +# This module supports the following states: +# +# Merged: +# Security Group Associations defined in the playbook will be merged into the target fabric. +# +# The Security Group Associations listed in the playbook will be created if not already present on the DCNM +# server. If the Security Group Association is already present and the configuration information included +# in the playbook is either different or not present in DCNM, then the corresponding +# information is added to the DCNM. If a Security Group Asssociation mentioned in playbook +# is already present on DCNM and there is no difference in configuration, no operation +# will be performed for such groups. +# +# Replaced: +# Security Group Associations defined in the playbook will be replaced in the target fabric. +# +# The state of the Security Group Associations listed in the playbook will serve as source of truth for the +# same Security Group Associations present on the DCNM under the fabric mentioned. Additions and updations +# will be done to bring the DCNM Security Group Associations to the state listed in the playbook. +# Note: Replace will only work on the Security Group Associations mentioned in the playbook. +# +# Overridden: +# Security Group Associations defined in the playbook will be overridden in the target fabric. +# +# The state of the Security Group Associations listed in the playbook will serve as source of truth for all +# the Security Group Associations under the fabric mentioned. Additions and deletions will be done to bring +# the DCNM Security Group Associations to the state listed in the playbook. All Security Group Associations other than the +# ones mentioned in the playbook will be deleted. +# Note: Override will work on the all the Security Group Associations present in the DCNM Fabric. +# +# Deleted: +# Security Group Associations defined in the playbook will be deleted in the target fabric. +# +# Deletes the list of Security Group Associations specified in the playbook. If the playbook does not include +# any Security Group Association information, then all Security Group Associations from the fabric will be deleted. +# +# Query: +# Returns the current DCNM state for the Security Group Associations listed in the playbook. + +# CREATE SECURITY GROUP ASSOCIATIONS + +- name: Create Security Group Associations - with and without mentioning group IDs + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + deploy: switches # choose from ["none", "switches"] + state: merged # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15001" + dst_group_name: "LSG_15001" + src_group_id: 15001 # Group Id associated with src_group_name + dst_group_id: 15001 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50001" + contract_name: CONTRACT1 + switch: + - 192.168.1.1 + - 192.168.1.2 + + - src_group_name: "LSG_15002" + dst_group_name: "LSG_15002" + vrf_name: "MyVRF_50002" + contract_name: CONTRACT1 + switch: + - 192.168.1.1 + - 192.168.1.2 + +# DELETE SECURITY GROUP ASSOCIATIONS + +- name: Delete Security Group Associations - without config + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + +- name: Delete Security Group Associations - with group name + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + config: + - src_group_name: "LSG_15001" + switch: + - 192.168.1.1 + +- name: Delete Security Group Associations - with group Id + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + config: + - dst_group_id: 15001 + +- name: Delete Security Group Associations - with vrf name + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + config: + - vrf_name: "MyVRF_50003" + switch: + - 192.168.1.2 + +- name: Delete Security Group Associations - with contract name + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + config: + - contract_name: "CONTRACT1" + +- name: Delete Security Group Associations - sepcifying all + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + config: + - src_group_id: 15001 + dst_group_id: 15002 + src_group_name: "LSG_15001" + dst_group_name: "LSG_15002" + vrf_name: "MyVRF_50003" + contract_name: "CONTRACT1" + +# REPLACE SECURITY GROUP ASSOCIATIONS + +- name: Replace Security Group Associations + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + deploy: switches # choose from ["none", "switches"] + state: replaced # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15001" + dst_group_name: "LSG_15001" + src_group_id: 15001 # Group Id associated with src_group_name + dst_group_id: 15001 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50001" + contract_name: ICMP-PERMIT + switch: + - 192.168.1.1 + - 192.168.1.2 + +# OVERRIDE SECURITY GROUP ASSOCIATIONS + +- name: Override Security Group Association without no config + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + deploy: switches # choose from ["none", "switches"] + state: overridden # choose from [merged, replaced, deleted, overridden, query] + +- name: Override Security Group Association with config + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + deploy: switches # choose from ["none", "switches"] + state: overridden # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15003" + dst_group_name: "LSG_15004" + src_group_id: 15003 # Group Id associated with src_group_name + dst_group_id: 15004 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50003" + contract_name: CONTRACT1 + switch: + - 192.168.1.1 + - 192.168.1.2 + +# QUERY SECURITY GROUP ASSOCIATIONS + +- name: Query Security Groups - without filters + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + deploy: none + state: query + +- name: Query Security Groups - with destination group name + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + deploy: none + state: query + config: + - dst_group_name: "LSG_15002" + +- name: Query Security Groups - with vrf name + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + deploy: none + state: query + config: + - vrf_name: "MyVRF_50003" + +- name: Query Security Groups - with group id + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + deploy: none + state: query + config: + - src_group_id: 15001 + +- name: Query Security Groups - with contract name + cisco.dcnm.dcnm_sgrp_association: + fabric: Test-Fabric + deploy: none + state: query + config: + - contract_name: CONTRACT1 +""" + +# +# WARNING: +# This file is automatically generated. Take a backup of your changes to this file before +# manually running cg_run.py script to generate it again +# + +import copy +import logging + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import ( + Log, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import ( + ResponseHandler, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import ( + RestSend, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_dcnm import ( + Sender, +) + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.common_utils import ( + Version, + InventoryData, + FabricInfo, + SwitchInfo, +) + +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + validate_list_of_dicts, + dcnm_get_ip_addr_info, +) + +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm_sgrp_association_utils import ( + Paths, + dcnm_sgrp_association_utils_get_sgrp_association_info, + dcnm_sgrp_association_utils_get_sgrp_association_payload, + dcnm_sgrp_association_utils_update_sgrp_association_information, + dcnm_sgrp_association_utils_get_matching_have, + dcnm_sgrp_association_utils_get_matching_cfg, + dcnm_sgrp_association_utils_compare_want_and_have, + dcnm_sgrp_association_utils_get_sgrp_association_deploy_payload, + dcnm_sgrp_association_utils_process_delete_payloads, + dcnm_sgrp_association_utils_process_create_payloads, + dcnm_sgrp_association_utils_process_modify_payloads, + dcnm_sgrp_association_utils_process_deploy_payloads, + dcnm_sgrp_association_utils_get_delete_payload, + dcnm_sgrp_association_utils_translate_sgrp_association_info, + dcnm_sgrp_association_utils_get_sync_status, + dcnm_sgrp_association_utils_get_delete_list, + dcnm_sgrp_association_utils_get_all_filtered_sgrp_association_objects, + dcnm_sgrp_association_utils_update_deploy_info, + dcnm_sgrp_association_utils_check_if_meta, + dcnm_sgrp_association_utils_validate_devices, +) + + +# Resource Class object which includes all the required methods and data to configure and maintain Security group associations +class DcnmSgrpAssociation: + def __init__(self, module): + + self.class_name = self.__class__.__name__ + + self.log = logging.getLogger(f"dcnm.{self.class_name}") + + self.module = module + self.params = module.params + self.fabric = module.params["fabric"] + self.deploy = module.params["deploy"] + self.config = copy.deepcopy(module.params.get("config", [])) + self.sgrp_info = {} + self.sync_info = {} + self.sgrp_association_info = [] + self.sgrp_association_list = [] + self.want = [] + self.have = [] + self.diff_create = [] + self.diff_modify = [] + self.diff_delete = [] + self.diff_delete_deploy = {} + self.diff_deploy = {} + self.monitoring = [] + self.meta_switches = [] + self.arg_specs = {} + self.fd = None + self.changed_dict = [ + { + "merged": [], + "deleted": [], + "delete_deploy": [], + "modified": [], + "query": [], + "deploy": [], + "debugs": [], + } + ] + + self.result = dict(changed=False, diff=[], response=[]) + + def dcnm_sgrp_association_get_diff_query(self): + + """ + Routine to retrieve Security group associations from controller. This routine extracts information provided by the + user and filters the output based on that. + + Parameters: + None + + Returns: + None + """ + + sgrp_association_list = dcnm_sgrp_association_utils_get_all_filtered_sgrp_association_objects( + self + ) + if sgrp_association_list != []: + self.result["response"].extend(sgrp_association_list) + + def dcnm_sgrp_association_get_diff_overridden(self, cfg): + + """ + Routine to override existing Security group associations information with what is included in the playbook. This routine + deletes all Security group associations objects which are not part of the current config and creates new ones based on what is + included in the playbook + + Parameters: + cfg (dct): Configuration information from playbook + + Returns: + None + """ + + del_list = dcnm_sgrp_association_utils_get_delete_list(self) + + # 'del_list' contains all Security group associations information in 'have' format. Use that to update delete and delete + # deploy payloads + + for elem in del_list: + self.dcnm_sgrp_association_update_delete_payloads(elem) + + if cfg == []: + return + + if self.want: + # New configuration is included. Delete all existing Security group associations objects and create new objects as requested + # through the configuration + self.dcnm_sgrp_association_get_diff_merge() + + def dcnm_sgrp_association_get_diff_deleted(self): + + """ + Routine to get a list of payload information that will be used to delete Security Group Associations. + This routine updates self.diff_delete with payloads that are used to delete Security Group Associations + from the server. + + Parameters: + None + + Returns: + None + """ + + if self.sgrp_association_info == []: + # User has not included any config. Delete all existing Security group associations objects from DCNM + self.dcnm_sgrp_association_get_diff_overridden([]) + return + + for elem in self.sgrp_association_info: + + # Perform any translations that may be required on the sgrp_association_info. + xelem = dcnm_sgrp_association_utils_translate_sgrp_association_info( + self, elem + ) + have = dcnm_sgrp_association_utils_get_sgrp_association_info( + self, xelem + ) + + if have != []: + # The above get routine would have matched all required keys. The only one to be matched here + # is the contract_name to filter the output as required. + if elem.get("contract_name", None) is not None: + if have["contractName"] == elem["contract_name"]: + self.dcnm_sgrp_association_update_delete_payloads(have) + else: + self.dcnm_sgrp_association_update_delete_payloads(have) + + def dcnm_sgrp_association_update_delete_payloads(self, have): + + # Get the delete payload based on 'have' + del_payload = dcnm_sgrp_association_utils_get_delete_payload( + self, have + ) + + if del_payload not in self.diff_delete: + self.changed_dict[0]["deleted"].append(del_payload) + self.diff_delete.append(del_payload) + + if self.deploy != "none": + # A delete of security group information must be followed by deploy to clean up VRFs and Networks. + dcnm_sgrp_association_utils_update_deploy_info( + self, have, self.diff_delete_deploy + ) + self.changed_dict[0]["delete_deploy"] = copy.deepcopy( + self.diff_delete_deploy + ) + + def dcnm_sgrp_association_get_diff_merge(self): + + """ + Routine to populate a list of payload information in self.diff_create to create/update Security Group Associations. + + Parameters: + None + + Returns: + None + """ + + for elem in self.want: + + rc, reasons, have = dcnm_sgrp_association_utils_compare_want_and_have( + self, elem + ) + + self.log.info( + f"Compare Want and Have: Return Code = {0}, Reasons = {1}, Have = {2}\n".format(rc, reasons, have) + ) + + if rc == "DCNM_SGRP_ASSOCIATION_CREATE": + # Object does not exists, create a new one. + if elem not in self.diff_create: + self.changed_dict[0]["merged"].append(elem) + self.diff_create.append(elem) + if rc == "DCNM_SGRP_ASSOCIATION_MERGE": + # Object already exists, and needs an update + if elem not in self.diff_modify: + self.changed_dict[0]["modified"].append(elem) + self.changed_dict[0]["debugs"].append({"REASONS": reasons}) + self.diff_modify.append(elem) + # Modifying existing Security group association requires UUID. Copy that + # from 'have'. + elem["uuid"] = have["uuid"] + + # Check if "deploy" flag is True. If True, deploy the changes. + if self.deploy != "none": + # Before building deploy payload, check + # - if something is being created + # - if something that is existing is being updated + # - if switches are in "In-Sync" state already + + if self.sync_info == {}: + self.sync_info = dcnm_sgrp_association_utils_get_sync_status( + self + ) + + dcnm_sgrp_association_utils_get_sgrp_association_deploy_payload( + self, elem, rc + ) + elem.pop("switch") + + if self.diff_deploy != {}: + self.changed_dict[0]["deploy"].append( + copy.deepcopy(self.diff_deploy) + ) + + def dcnm_sgrp_association_update_want(self): + + """ + This routine does the following when the state is 'merged'. For every object in self.want + + - Find a matching object from self.have + - Find a matching object from the playbook configuration + - Invoke update function which updates 'want' appropriatley based on the matching objects found + + Parameters: + None + + Returns: + None + """ + + if self.module.params["state"] != "merged": + return + + match_have = [] + match_cfg = [] + for want in self.want: + + match_have = dcnm_sgrp_association_utils_get_matching_have( + self, want + ) + + # If have is [], then there is nothing to update + if match_have != []: + match_cfg = dcnm_sgrp_association_utils_get_matching_cfg( + self, want + ) + if match_cfg == []: + continue + + for melem in match_have: + dcnm_sgrp_association_utils_update_sgrp_association_information( + self, want, melem, match_cfg[0] + ) + + def dcnm_sgrp_association_get_want(self): + + """ + This routine updates self.want with the payload information based on the playbook configuration. + + Parameters: + None + + Returns: + None + """ + + if self.config == []: + return + + for elem in self.sgrp_association_info: + + # If a separate payload is required for every switch included in the payload, then modify this + # code to loop over the switches. Also the get payload routine should be modified appropriately. + + payload = self.dcnm_sgrp_association_get_payload(elem) + if payload and payload not in self.want: + self.want.append(payload) + + def dcnm_sgrp_association_get_have(self): + + """ + Routine to get exisitng sgrp_association information from DCNM that matches information in self.want. + This routine updates self.have with all the sgrp_association that match the given playbook configuration + + Parameters: + None + + Returns: + None + """ + + if self.want == []: + return + + for elem in self.want: + have = dcnm_sgrp_association_utils_get_sgrp_association_info( + self, elem + ) + if (have != []) and (have not in self.have): + self.have.append(have) + + def dcnm_sgrp_association_validate_deleted_state_input(self, cfg): + + """ + Playbook input will be different for differnt states. This routine validates the + deleted state input. This routine updates self.sgrp_association_info with + validated playbook information related to deleted state. + + Parameters: + cfg (dict): The config from playbook + + Returns: + None + """ + + arg_spec = { + "src_group_id": { + "type": "int", + "range_min": 16, + "range_max": 65535, + }, + "dst_group_id": { + "type": "int", + "range_min": 16, + "range_max": 65535, + }, + "src_group_name": {"type": "str"}, + "dst_group_name": {"type": "str"}, + "contract_name": {"type": "str"}, + "vrf_name": {"type": "str"}, + } + + sgrp_association_info, invalid_params = validate_list_of_dicts( + cfg, arg_spec + ) + if invalid_params: + mesg = "Invalid parameters in playbook: {0}".format(invalid_params) + self.module.fail_json(msg=mesg) + + if sgrp_association_info: + self.sgrp_association_info.extend(sgrp_association_info) + + def dcnm_sgrp_association_validate_query_state_input(self, cfg): + + """ + Playbook input will be different for differnt states. This routine validates the + query state input. This routine updates self.sgrp_association_info with + validated playbook information related to query state. + + Parameters: + cfg (dict): The config from playbook + + Returns: + None + """ + + arg_spec = { + "src_group_id": { + "type": "int", + "range_min": 16, + "range_max": 65535, + }, + "dst_group_id": { + "type": "int", + "range_min": 16, + "range_max": 65535, + }, + "src_group_name": {"type": "str"}, + "dst_group_name": {"type": "str"}, + "contract_name": {"type": "str"}, + "vrf_name": {"type": "str"}, + } + + sgrp_association_info, invalid_params = validate_list_of_dicts( + cfg, arg_spec + ) + if invalid_params: + mesg = "Invalid parameters in playbook: {0}".format(invalid_params) + self.module.fail_json(msg=mesg) + + if sgrp_association_info: + self.sgrp_association_info.extend(sgrp_association_info) + + def dcnm_sgrp_association_validate_input(self, cfg): + + # The generator hanldes only the case where: + # - there are some common paremeters that are included in the playbook + # - and a profile which is a 'dict' and which is either based on a template or some fixed structure + # NOTE: This code assumes that the nested structure will be under a key called 'profile'. If not modify the + # same appropriately. + # This routine generates code to validate the common part and the 'profile' part which is one level nested. + # Users must modify this code appropriately to hanlde any further nested structures that may be part + # of playbook input. + + common_spec = { + "src_group_id": { + "type": "int", + "range_min": 16, + "range_max": 65535, + }, + "dst_group_id": { + "type": "int", + "range_min": 16, + "range_max": 65535, + }, + "src_group_name": {"required": True, "type": "str"}, + "dst_group_name": {"required": True, "type": "str"}, + "vrf_name": {"required": True, "type": "str"}, + "contract_name": {"required": True, "type": "str"}, + "switch": {"required": True, "type": "list", "elements": "str"}, + } + + sgrp_association_info, invalid_params = validate_list_of_dicts( + cfg, common_spec + ) + if invalid_params: + mesg = "Invalid parameters in playbook: {0}".format(invalid_params) + self.module.fail_json(msg=mesg) + + self.sgrp_association_info.append(sgrp_association_info[0]) + + def dcnm_sgrp_association_validate_all_input(self): + + """ + Routine to validate playbook input based on the state. Since each state has a different + config structure, this routine handles the validation based on the given state + + Parameters: + None + + Returns: + None + """ + + if self.config == []: + return + + cfg = [] + for item in self.config: + + citem = copy.deepcopy(item) + + cfg.append(citem) + + if self.module.params["state"] == "query": + # config for query state is different. So validate query state differently + self.dcnm_sgrp_association_validate_query_state_input(cfg) + elif self.module.params["state"] == "deleted": + # config for deleted state is different. So validate deleted state differently + self.dcnm_sgrp_association_validate_deleted_state_input(cfg) + else: + self.dcnm_sgrp_association_validate_input(cfg) + cfg.remove(citem) + + def dcnm_sgrp_association_get_payload(self, sgrp_association_info): + + """ + This routine builds the complete object payload based on the information in self.want + + Parameters: + sgrp_association_info (dict): Object information + + Returns: + sgrp_association_payload (dict): Object payload information populated with appropriate data from playbook config + """ + + sgrp_association_payload = dcnm_sgrp_association_utils_get_sgrp_association_payload( + self, sgrp_association_info + ) + + return sgrp_association_payload + + def dcnm_sgrp_association_update_switch_info(self): + + """ + Routine to update inventory data for all fabrics included in the playbook. This routine + also updates ip_sn, sn_hn and hn_sn objetcs from the updated inventory data. + + Parameters: + None + + Returns: + None + """ + + try: + switch_info = SwitchInfo() + switch_info.inventory_data = self.inventory_data + switch_info.fabric = self.fabric + switch_info.commit() + except ValueError as error: + self.module.fail_json(msg=f"{str(error)}") + + self.managable = switch_info.managable + self.meta_switches = switch_info.meta_switches + self.ip_sn = switch_info.ip_sn + self.hn_sn = switch_info.hn_sn + self.sn_hn = switch_info.sn_hn + self.sn_ip = switch_info.sn_ip + + def dcnm_sgrp_association_translate_playbook_info( + self, config, ip_sn, hn_sn + ): + + """ + Routine to translate parameters in playbook if required. + - This routine converts the hostname information included in + playbook to actual addresses. + + Parameters: + config - The resource which needs translation + ip_sn - IP address to serial number mappings + hn_sn - hostname to serial number mappings + + Returns: + None + """ + + if config == []: + return + + for cfg in config: + index = 0 + if cfg.get("switch", None) is None: + continue + for sw_elem in cfg["switch"][:]: + if ( + dcnm_sgrp_association_utils_check_if_meta(self, sw_elem) + is True + ): + continue + if sw_elem in self.ip_sn or sw_elem in self.hn_sn: + addr_info = dcnm_get_ip_addr_info( + self.module, sw_elem, ip_sn, hn_sn + ) + cfg["switch"][index] = addr_info + else: + cfg["switch"].remove(sw_elem) + index = index + 1 + + if cfg.get("switch", None) is not None: + # Check if the switches included in the config are Manageable. + dcnm_sgrp_association_utils_validate_devices(self, cfg) + + def dcnm_sgrp_association_send_message_to_dcnm(self): + + """ + Routine to push payloads to DCNM server. This routine implements required error checks and retry mechanisms to handle + transient errors. This routine checks self.diff_create, self.diff_delete lists and push appropriate requests to DCNM. + + Parameters: + None + + Returns: + None + """ + + resp = None + create_flag = False + modify_flag = False + delete_flag = False + deploy_flag = False + + delete_flag = dcnm_sgrp_association_utils_process_delete_payloads(self) + create_flag = dcnm_sgrp_association_utils_process_create_payloads(self) + modify_flag = dcnm_sgrp_association_utils_process_modify_payloads(self) + deploy_flag = dcnm_sgrp_association_utils_process_deploy_payloads( + self, self.diff_deploy + ) + + self.log.debug( + f"Flags: CR = {0}, DL = {1}, MO = {2}, DP = {3}\n". + format(create_flag, delete_flag, modify_flag, deploy_flag) + ) + + self.result["changed"] = ( + create_flag or modify_flag or delete_flag or deploy_flag + ) + + def dcnm_sgrp_association_update_module_info(self): + + """ + Routine to update version and fabric details + + Parameters: + None + + Returns: + None + """ + + try: + version = Version() + version.module = self.module + version.commit() + self.dcnm_version = version.dcnm_version + except ValueError as error: + self.module.fail_json(msg=f"{str(error)}") + + try: + inv_data = InventoryData() + inv_data.module = self.module + inv_data.fabric = self.fabric + inv_data.commit() + self.inventory_data = inv_data.inventory_data + except ValueError as error: + self.module.fail_json(msg=f"{str(error)}") + + try: + paths = Paths() + paths.version = self.dcnm_version + paths.commit() + self.paths = paths.paths + except ValueError as error: + self.module.fail_json(msg=f"{str(error)}") + + try: + fabric_info = FabricInfo() + fabric_info.module = self.module + fabric_info.fabric = self.fabric + fabric_info.rest_send = self.rest_send + fabric_info.paths = self.paths + fabric_info.commit() + except ValueError as error: + self.module.fail_json(msg=f"{str(error)}") + + +def main(): + + """ main entry point for module execution + """ + element_spec = dict( + fabric=dict(required=True, type="str"), + config=dict(type="list", elements="dict", default=[]), + state=dict( + type="str", + default="merged", + choices=["merged", "deleted", "replaced", "overridden", "query"], + ), + deploy=dict( + type="str", choices=["none", "switches"], default="switches" + ), + ) + + module = AnsibleModule( + argument_spec=element_spec, supports_check_mode=True + ) + + dcnm_sgrp_association = DcnmSgrpAssociation(module) + + state = module.params["state"] + + dcnm_sgrp_association.log.debug( + f"######################### BEGIN STATE = {0} ##########################\n".format(state) + ) + + # Initialize the logger + try: + logger = Log() + logger.commit() + except ValueError as error: + module.fail_json(msg=str(error)) + + # Initialize the Sender object + sender = Sender() + sender.ansible_module = module + dcnm_sgrp_association.rest_send = RestSend(module.params) + dcnm_sgrp_association.rest_send.response_handler = ResponseHandler() + dcnm_sgrp_association.rest_send.sender = sender + + # Fill up the version and fabric related details + dcnm_sgrp_association.dcnm_sgrp_association_update_module_info() + + if dcnm_sgrp_association.config == []: + if state == "merged" or state == "replaced": + module.fail_json( + msg="'config' element is mandatory for state '{0}', given = '{1}'".format( + state, dcnm_sgrp_association.config + ) + ) + + dcnm_sgrp_association.dcnm_sgrp_association_update_switch_info() + + dcnm_sgrp_association.dcnm_sgrp_association_translate_playbook_info( + dcnm_sgrp_association.config, + dcnm_sgrp_association.ip_sn, + dcnm_sgrp_association.hn_sn, + ) + + dcnm_sgrp_association.dcnm_sgrp_association_validate_all_input() + + dcnm_sgrp_association.log.info( + f"Config Info = {0}\n".format(dcnm_sgrp_association.config) + ) + dcnm_sgrp_association.log.info( + f"Validated Security Group Association Info = {0}\n".format(dcnm_sgrp_association.sgrp_association_info) + ) + + if ( + module.params["state"] != "query" + and module.params["state"] != "deleted" + ): + dcnm_sgrp_association.dcnm_sgrp_association_get_want() + + dcnm_sgrp_association.log.info( + f"Want = {0}\n".format(dcnm_sgrp_association.want) + ) + dcnm_sgrp_association.dcnm_sgrp_association_get_have() + + dcnm_sgrp_association.log.info( + f"Have = {0}\n".format(dcnm_sgrp_association.have) + ) + + # self.want would have defaulted all optional objects not included in playbook. But the way + # these objects are handled is different between 'merged' and 'replaced' states. For 'merged' + # state, objects not included in the playbook must be left as they are and for state 'replaced' + # they must be purged or defaulted. + + dcnm_sgrp_association.dcnm_sgrp_association_update_want() + dcnm_sgrp_association.log.info( + f"Updated Want = {0}\n".format(dcnm_sgrp_association.want) + ) + + dcnm_sgrp_association.log.info( + f"Security Groups Info = {0}\n".format(dcnm_sgrp_association.sgrp_info) + ) + if (module.params["state"] == "merged") or ( + module.params["state"] == "replaced" + ): + dcnm_sgrp_association.dcnm_sgrp_association_get_diff_merge() + + if module.params["state"] == "deleted": + dcnm_sgrp_association.dcnm_sgrp_association_get_diff_deleted() + + if module.params["state"] == "overridden": + dcnm_sgrp_association.dcnm_sgrp_association_get_diff_overridden( + dcnm_sgrp_association.config + ) + + if module.params["state"] == "query": + dcnm_sgrp_association.dcnm_sgrp_association_get_diff_query() + + dcnm_sgrp_association.log.info( + f"Create Info = {0}\n".format(dcnm_sgrp_association.diff_create) + ) + dcnm_sgrp_association.log.info( + f"Replace Info = {0}\n".format(dcnm_sgrp_association.diff_modify) + ) + dcnm_sgrp_association.log.info( + f"Delete Info = {0}\n".format(dcnm_sgrp_association.diff_delete) + ) + dcnm_sgrp_association.log.info( + f"Deploy Info = {0}\n".format(dcnm_sgrp_association.diff_deploy) + ) + dcnm_sgrp_association.log.info( + f"Delete Deploy Info = {0}\n".format(dcnm_sgrp_association.diff_delete_deploy) + ) + + dcnm_sgrp_association.result["diff"] = dcnm_sgrp_association.changed_dict + dcnm_sgrp_association.changed_dict[0]["debugs"].append( + {"Managable": dcnm_sgrp_association.managable} + ) + dcnm_sgrp_association.changed_dict[0]["debugs"].append( + {"Monitoring": dcnm_sgrp_association.monitoring} + ) + + dcnm_sgrp_association.changed_dict[0]["debugs"].append( + {"Meta_Switches": dcnm_sgrp_association.meta_switches} + ) + + if dcnm_sgrp_association.diff_create or dcnm_sgrp_association.diff_delete: + dcnm_sgrp_association.result["changed"] = True + + if module.check_mode: + dcnm_sgrp_association.result["changed"] = False + module.exit_json(**dcnm_sgrp_association.result) + + dcnm_sgrp_association.dcnm_sgrp_association_send_message_to_dcnm() + + dcnm_sgrp_association.log.debug( + f"######################### END STATE = {0} ##########################\n".format(state) + ) + + module.exit_json(**dcnm_sgrp_association.result) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/dcnm_sgrp_association/defaults/main.yaml b/tests/integration/targets/dcnm_sgrp_association/defaults/main.yaml new file mode 100644 index 000000000..5f709c5aa --- /dev/null +++ b/tests/integration/targets/dcnm_sgrp_association/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/tests/integration/targets/dcnm_sgrp_association/meta/main.yaml b/tests/integration/targets/dcnm_sgrp_association/meta/main.yaml new file mode 100644 index 000000000..8b4c40b7b --- /dev/null +++ b/tests/integration/targets/dcnm_sgrp_association/meta/main.yaml @@ -0,0 +1,2 @@ +dependencies: +# - prepare_dcnm_policy diff --git a/tests/integration/targets/dcnm_sgrp_association/tasks/dcnm.yaml b/tests/integration/targets/dcnm_sgrp_association/tasks/dcnm.yaml new file mode 100644 index 000000000..17758f728 --- /dev/null +++ b/tests/integration/targets/dcnm_sgrp_association/tasks/dcnm.yaml @@ -0,0 +1,24 @@ +--- +- name: collect dcnm test cases + find: + paths: "{{ role_path }}/tests/dcnm" + patterns: "{{ testcase }}.yaml" + connection: local + register: dcnm_cases + tags: sanity + +- set_fact: + test_cases: + files: "{{ dcnm_cases.files }}" + tags: sanity + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + tags: sanity + +- name: run test cases (connection=httpapi) + include_tasks: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run + tags: sanity diff --git a/tests/integration/targets/dcnm_sgrp_association/tasks/main.yaml b/tests/integration/targets/dcnm_sgrp_association/tasks/main.yaml new file mode 100644 index 000000000..fbcfa5803 --- /dev/null +++ b/tests/integration/targets/dcnm_sgrp_association/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include_tasks: dcnm.yaml, tags: ['dcnm'] } diff --git a/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_delete.yaml b/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_delete.yaml new file mode 100644 index 000000000..0a83a9a13 --- /dev/null +++ b/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_delete.yaml @@ -0,0 +1,273 @@ +############################################## +## SETUP ## +############################################## + +- name: Remove local log file + local_action: command rm -f dcnm_sgrp_association.log + +- name: Put the fabric to default state + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + register: result + +- assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' + + +- block: + +############################################## +## MERGE ## +############################################## + + - name: Create Security Group Associations + cisco.dcnm.dcnm_sgrp_association: &sgrp_assoc_create + fabric: "{{ ansible_it_fabric }}" + deploy: switches # choose from ["none", "switches"] + state: merged # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15001" + dst_group_name: "LSG_15001" + src_group_id: 15001 # Group Id associated with src_group_name + dst_group_id: 15001 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50001" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + + - src_group_name: "LSG_15002" + dst_group_name: "LSG_15002" + vrf_name: "MyVRF_50002" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + + - src_group_name: "LSG_15003" + dst_group_name: "LSG_15003" + vrf_name: "MyVRF_50003" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + + - src_group_name: "LSG_15004" + dst_group_name: "LSG_15004" + vrf_name: "MyVRF_50004" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + + - src_group_name: "LSG_15005" + dst_group_name: "LSG_15005" + vrf_name: "MyVRF_50005" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 5' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 1' + +############################################## +## DELETE ## +############################################## + + - name: Delete Security Group Associations - without config + cisco.dcnm.dcnm_sgrp_association: &sgrp_assoc_delete + fabric: "{{ ansible_it_fabric }}" + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 5' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 0' + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' + +############################################## +## IDEMPOTENCE ## +############################################## + + - name: Delete Security Group Associations - Idempotence + cisco.dcnm.dcnm_sgrp_association: *sgrp_assoc_delete + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 0' + +############################################## +## MERGE +############################################## + + - name: Create Security Group Associations again + cisco.dcnm.dcnm_sgrp_association: *sgrp_assoc_create + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 5' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 1' + + +############################################## +## DELETE ## +############################################## + + - name: Delete Security Group Associations - with source group name + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + config: + - src_group_name: "LSG_15001" + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 1' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 0' + + - name: Delete Security Group Associations - with destination group name + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + config: + - dst_group_name: "LSG_15002" + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 1' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 0' + + - name: Delete Security Group Associations - with vrf name + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + config: + - vrf_name: "MyVRF_50003" + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 1' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 0' + + - name: Delete Security Group Associations - with group id + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + config: + - src_group_id: 15005 + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 1' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 0' + + - name: Delete Security Group Associations - with contract name + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + config: + - contract_name: "CONTRACT1" + switch: + - "{{ ansible_switch1 }}" + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 1' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 0' + + - name: Delete Security Group Associations - non-existent associations + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: none # choose from ["none", "switches"] + config: + - src_group_name: "LSG_15001" + - dst_group_name: "LSG_15002" + - vrf_name: MyVRF_50003 + - contract_name: CONTRACT1 + - src_group_id: 15004 + - dst_group_id: 15005 + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 0' + +############################################## +## CLEANUP ## +############################################## + + always: + + - name: Delete all created security group associations + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + register: result + when: IT_CONTEXT is not defined + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - 'item["MESSAGE"] == "OK"' + loop: '{{ result.response }}' + when: IT_CONTEXT is not defined diff --git a/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_merge.yaml b/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_merge.yaml new file mode 100644 index 000000000..67ebc0253 --- /dev/null +++ b/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_merge.yaml @@ -0,0 +1,232 @@ +############################################## +## SETUP ## +############################################## + +- name: Remove local log file + local_action: command rm -f dcnm_sgrp_association.log + +- name: Put the fabric to default state + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + register: result + +- assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' + + +- block: + +############################################## +## MERGE ## +############################################## + + - name: Create Security Group Associations - with and without mentioning group IDs + cisco.dcnm.dcnm_sgrp_association: &sgrp_assoc_create + fabric: "{{ ansible_it_fabric }}" + deploy: switches # choose from ["none", "switches"] + state: merged # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15001" + dst_group_name: "LSG_15001" + src_group_id: 15001 # Group Id associated with src_group_name + dst_group_id: 15001 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50001" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + + - src_group_name: "LSG_15002" + dst_group_name: "LSG_15002" + vrf_name: "MyVRF_50002" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 2' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 1' + +############################################## +## IDEMPOTENCE ## +############################################## + + - name: Create Security Group Associations - Idempotence + cisco.dcnm.dcnm_sgrp_association: *sgrp_assoc_create + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 0' + + - name: Create Security Group Association without mentioning state + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + deploy: switches # choose from ["none", "switches"] + config: + - src_group_name: "LSG_15001" + dst_group_name: "LSG_15002" + src_group_id: 15001 # Group Id associated with src_group_name + dst_group_id: 15002 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50001" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 1' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 1' + + - name: Create Security Group Association - without deploy flag + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: merged # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15003" + dst_group_name: "LSG_15004" + src_group_id: 15003 # Group Id associated with src_group_name + dst_group_id: 15004 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50001" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 1' + - '(result["diff"][0]["modified"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 1' + + - name: Create Security Group Association - without src_group_name + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: merged # choose from [merged, replaced, deleted, overridden, query] + config: + - dst_group_name: "LSG_15004" + src_group_id: 15003 # Group Id associated with src_group_name + dst_group_id: 15004 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50001" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + register: result + ignore_errors: yes + + - assert: + that: + - 'result.changed == false' + - '("Invalid parameters in playbook" in result["msg"])' + - "('src_group_name : Required parameter not found' in result['msg'])" + + - name: Create Security Group Association - without dst_group_name + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: merged # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15003" + src_group_id: 15003 # Group Id associated with src_group_name + st_group_id: 15004 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50001" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + register: result + ignore_errors: yes + + - assert: + that: + - 'result.changed == false' + - '("Invalid parameters in playbook" in result["msg"])' + - "('dst_group_name : Required parameter not found' in result['msg'])" + + - name: Create Security Group Association - without vrf_name + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: merged # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15003" + dst_group_name: "LSG_15003" + src_group_id: 15003 # Group Id associated with src_group_name + dst_group_id: 15004 # Group Id associated with dst_group_name + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + register: result + ignore_errors: yes + + - assert: + that: + - 'result.changed == false' + - '("Invalid parameters in playbook" in result["msg"])' + - "('vrf_name : Required parameter not found' in result['msg'])" + + - name: Create Security Group Association - without contract_name + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: merged # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15003" + dst_group_name: "LSG_15004" + src_group_id: 15003 # Group Id associated with src_group_name + dst_group_id: 15004 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50001" + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + register: result + ignore_errors: yes + + - assert: + that: + - 'result.changed == false' + - '("Invalid parameters in playbook" in result["msg"])' + - "('contract_name : Required parameter not found' in result['msg'])" + +############################################## +## CLEANUP ## +############################################## + + always: + + - name: Delete all created security group associations + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + register: result + when: IT_CONTEXT is not defined + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - 'item["MESSAGE"] == "OK"' + loop: '{{ result.response }}' + when: IT_CONTEXT is not defined diff --git a/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_override.yaml b/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_override.yaml new file mode 100644 index 000000000..708e1625a --- /dev/null +++ b/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_override.yaml @@ -0,0 +1,213 @@ +############################################## +## SETUP ## +############################################## + +- name: Remove local log file + local_action: command rm -f dcnm_sgrp_association.log + +- name: Put the fabric to default state + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + register: result + +- assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' + + +- block: + +############################################## +## MERGE ## +############################################## + + - name: Create Security Group Associations - with and without mentioning group IDs + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + deploy: switches # choose from ["none", "switches"] + state: merged # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15001" + dst_group_name: "LSG_15001" + src_group_id: 15001 # Group Id associated with src_group_name + dst_group_id: 15001 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50001" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + + - src_group_name: "LSG_15002" + dst_group_name: "LSG_15002" + vrf_name: "MyVRF_50002" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 2' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 1' + +############################################## +## OVERRIDE ## +############################################## + + - name: Override Security Group Association without new config + cisco.dcnm.dcnm_sgrp_association: &sgrp_assoc_ovr + fabric: "{{ ansible_it_fabric }}" + deploy: switches # choose from ["none", "switches"] + state: overridden # choose from [merged, replaced, deleted, overridden, query] + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 2' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 0' + +############################################## +## IDEMPOTENCE ## +############################################## + + - name: Override Security Group Associations - Idempotence + cisco.dcnm.dcnm_sgrp_association: *sgrp_assoc_ovr + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 0' + +############################################## +## OVERRIDE ## +############################################## + + - name: Override Security Group Association with new config + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + deploy: switches # choose from ["none", "switches"] + state: overridden # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15003" + dst_group_name: "LSG_15004" + src_group_id: 15003 # Group Id associated with src_group_name + dst_group_id: 15004 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50003" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 1' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 1' + +############################################## +## OVERRIDE ## +############################################## + + - name: Override Security Group Association with existing config modified + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + deploy: switches # choose from ["none", "switches"] + state: overridden # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15003" + dst_group_name: "LSG_15004" + src_group_id: 15003 # Group Id associated with src_group_name + dst_group_id: 15004 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50003" + contract_name: ICMP-PERMIT + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["modified"] | length) == 1' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 1' + +############################################## +## OVERRIDE ## +############################################## + + - name: Override Security Group Association with existing and new config + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + deploy: switches # choose from ["none", "switches"] + state: overridden # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15001" + dst_group_name: "LSG_15002" + src_group_id: 15001 # Group Id associated with src_group_name + dst_group_id: 15002 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50001" + contract_name: ICMP-PERMIT + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + + - src_group_name: "LSG_15003" + dst_group_name: "LSG_15004" + src_group_id: 15003 # Group Id associated with src_group_name + dst_group_id: 15004 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50003" + contract_name: ICMP-PERMIT + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 1' + - '(result["diff"][0]["modified"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 1' + +############################################## +## CLEANUP ## +############################################## + + always: + + - name: Delete all created security group associations + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + register: result + when: IT_CONTEXT is not defined + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - 'item["MESSAGE"] == "OK"' + loop: '{{ result.response }}' + when: IT_CONTEXT is not defined diff --git a/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_query.yaml b/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_query.yaml new file mode 100644 index 000000000..8634c5073 --- /dev/null +++ b/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_query.yaml @@ -0,0 +1,229 @@ +############################################## +## SETUP ## +############################################## + +- name: Remove local log file + local_action: command rm -f dcnm_sgrp_association.log + +- name: Put the fabric to default state + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + register: result + +- assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' + + +- block: + +############################################## +## MERGE ## +############################################## + + - name: Create Security Group Associations - with and without mentioning group IDs + cisco.dcnm.dcnm_sgrp_association: &sgrp_assoc_create + fabric: "{{ ansible_it_fabric }}" + deploy: none # choose from ["none", "switches"] + state: merged # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15001" + dst_group_name: "LSG_15001" + src_group_id: 15001 # Group Id associated with src_group_name + dst_group_id: 15001 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50001" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + + - src_group_name: "LSG_15002" + dst_group_name: "LSG_15002" + vrf_name: "MyVRF_50002" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + + - src_group_name: "LSG_15003" + dst_group_name: "LSG_15003" + vrf_name: "MyVRF_50003" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + + - src_group_name: "LSG_15004" + dst_group_name: "LSG_15004" + vrf_name: "MyVRF_50004" + src_group_id: 15004 # Group Id associated with src_group_name + dst_group_id: 15004 # Group Id associated with dst_group_name + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + + - src_group_name: "LSG_15005" + dst_group_name: "LSG_15005" + vrf_name: "MyVRF_50005" + contract_name: CONTRACT2 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 5' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 0' + +############################################## +## QUERY ## +############################################## + + - name: Query Security Groups - without filters + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + deploy: switches + state: query + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["response"] | length) != 0' + + - name: Query Security Groups - with source group name + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + deploy: switches + state: query + config: + - src_group_name: "LSG_15001" + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["response"] | length) == 1' + - 'result["response"][0]["srcGroupName"] == "LSG_15001"' + + - name: Query Security Groups - with destination group name + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + deploy: switches + state: query + config: + - dst_group_name: "LSG_15002" + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["response"] | length) == 1' + - 'result["response"][0]["dstGroupName"] == "LSG_15002"' + + - name: Query Security Groups - with vrf name + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + deploy: switches + state: query + config: + - vrf_name: "MyVRF_50003" + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["response"] | length) == 1' + - 'result["response"][0]["vrfName"] == "MyVRF_50003"' + + - name: Query Security Groups - with contract name + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + deploy: switches + state: query + config: + - contract_name: CONTRACT2 + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["response"] | length) == 1' + - 'result["response"][0]["contractName"] == "CONTRACT2"' + + - name: Query Security Groups - with source group id + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + deploy: switches + state: query + config: + - src_group_id: 15001 + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["response"] | length) == 1' + - 'result["response"][0]["srcGroupId"] == 15001' + + - name: Query Security Groups - with destination group id + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + deploy: switches + state: query + config: + - dst_group_id: 15004 + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["response"] | length) == 1' + - 'result["response"][0]["dstGroupId"] == 15004' + + - name: Query Security Groups - non existing associations + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + deploy: switches + state: query + config: + - dst_group_id: 15014 + - src_group_id: 15011 + - vrf_name: "MyVRF_50013" + - src_group_name: "LSG_15012" + - dst_group_name: "LSG_15014" + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["response"] | length) == 0' + +############################################## +## CLEANUP ## +############################################## + + always: + + - name: Delete all created security group associations + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + register: result + when: IT_CONTEXT is not defined + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - 'item["MESSAGE"] == "OK"' + loop: '{{ result.response }}' + when: IT_CONTEXT is not defined diff --git a/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_replace.yaml b/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_replace.yaml new file mode 100644 index 000000000..a6a0d563d --- /dev/null +++ b/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_replace.yaml @@ -0,0 +1,135 @@ +############################################## +## SETUP ## +############################################## + +- name: Remove local log file + local_action: command rm -f dcnm_sgrp_association.log + +- name: Put the fabric to default state + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + register: result + +- assert: + that: + - 'item["RETURN_CODE"] == 200' + loop: '{{ result.response }}' + + +- block: + +############################################## +## MERGE ## +############################################## + + - name: Create Security Group Associations - with replaced state + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + deploy: switches # choose from ["none", "switches"] + state: replaced # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15001" + dst_group_name: "LSG_15001" + src_group_id: 15001 # Group Id associated with src_group_name + dst_group_id: 15001 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50001" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + + - src_group_name: "LSG_15002" + dst_group_name: "LSG_15002" + vrf_name: "MyVRF_50002" + contract_name: CONTRACT1 + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 2' + - '(result["diff"][0]["modified"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 1' + +############################################## +## REPLACED ## +############################################## + + - name: Replace Security Group Associations + cisco.dcnm.dcnm_sgrp_association: &sgrp_assoc_replace + fabric: "{{ ansible_it_fabric }}" + deploy: switches # choose from ["none", "switches"] + state: replaced # choose from [merged, replaced, deleted, overridden, query] + config: + - src_group_name: "LSG_15001" + dst_group_name: "LSG_15001" + src_group_id: 15001 # Group Id associated with src_group_name + dst_group_id: 15001 # Group Id associated with dst_group_name + vrf_name: "MyVRF_50001" + contract_name: ICMP-PERMIT + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + + - src_group_name: "LSG_15002" + dst_group_name: "LSG_15002" + vrf_name: "MyVRF_50002" + contract_name: ICMP-PERMIT + switch: + - "{{ ansible_switch1 }}" + - "{{ ansible_switch2 }}" + register: result + + - assert: + that: + - 'result.changed == true' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["modified"] | length) == 2' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 1' + +############################################## +## IDEMPOTENCE ## +############################################## + + - name: Create Security Group Associations - Idempotence + cisco.dcnm.dcnm_sgrp_association: *sgrp_assoc_replace + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["modified"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 0' + +############################################## +## CLEANUP ## +############################################## + + always: + + - name: Delete all created security group associations + cisco.dcnm.dcnm_sgrp_association: + fabric: "{{ ansible_it_fabric }}" + state: deleted # choose from [merged, replaced, deleted, overridden, query] + deploy: switches # choose from ["none", "switches"] + register: result + when: IT_CONTEXT is not defined + + - assert: + that: + - 'item["RETURN_CODE"] == 200' + - 'item["MESSAGE"] == "OK"' + loop: '{{ result.response }}' + when: IT_CONTEXT is not defined diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index 60d9043d3..95eef6e02 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -16,5 +16,6 @@ plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_sgrp_association.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_rest.py import-2.6!skip plugins/modules/dcnm_rest.py import-2.7!skip diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index 4723c583b..a69ed8acb 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -16,6 +16,7 @@ plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_sgrp_association.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_rest.py import-2.6!skip plugins/modules/dcnm_rest.py import-2.7!skip plugins/httpapi/dcnm.py import-2.7!skip diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt index 334160f16..5e0a3d7a1 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -16,6 +16,7 @@ plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_sgrp_association.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_rest.py import-2.6!skip plugins/modules/dcnm_rest.py import-2.7!skip plugins/httpapi/dcnm.py import-3.8!skip diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt index b535a3144..8afd2a9a9 100644 --- a/tests/sanity/ignore-2.13.txt +++ b/tests/sanity/ignore-2.13.txt @@ -17,6 +17,7 @@ plugins/httpapi/dcnm.py validate-modules:missing-gplv3-license # GPLv3 license h plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_sgrp_association.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_rest.py import-2.7!skip plugins/httpapi/dcnm.py import-3.8!skip plugins/httpapi/dcnm.py import-3.9!skip diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt index 15705d33b..99e749061 100644 --- a/tests/sanity/ignore-2.14.txt +++ b/tests/sanity/ignore-2.14.txt @@ -17,6 +17,7 @@ plugins/httpapi/dcnm.py validate-modules:missing-gplv3-license # GPLv3 license h plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_sgrp_association.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_rest.py import-2.7!skip plugins/httpapi/dcnm.py import-3.9!skip plugins/httpapi/dcnm.py import-3.10!skip diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt index 15705d33b..99e749061 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -17,6 +17,7 @@ plugins/httpapi/dcnm.py validate-modules:missing-gplv3-license # GPLv3 license h plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_sgrp_association.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_rest.py import-2.7!skip plugins/httpapi/dcnm.py import-3.9!skip plugins/httpapi/dcnm.py import-3.10!skip diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt index 20cfc7582..30f563495 100644 --- a/tests/sanity/ignore-2.16.txt +++ b/tests/sanity/ignore-2.16.txt @@ -17,3 +17,4 @@ plugins/httpapi/dcnm.py validate-modules:missing-gplv3-license # GPLv3 license h plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_sgrp_association.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 60d9043d3..95eef6e02 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -16,5 +16,6 @@ plugins/modules/dcnm_vpc_pair.py validate-modules:missing-gplv3-license # GPLv3 plugins/modules/dcnm_image_upload.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_sgrp_association.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_rest.py import-2.6!skip plugins/modules/dcnm_rest.py import-2.7!skip diff --git a/tests/unit/modules/dcnm/fixtures/common/common_responses.json b/tests/unit/modules/dcnm/fixtures/common/common_responses.json new file mode 100644 index 000000000..900cc8618 --- /dev/null +++ b/tests/unit/modules/dcnm/fixtures/common/common_responses.json @@ -0,0 +1,129 @@ +{ + "access_mode_resp": + { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://10.78.210.227:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/unit-test/accessmode", + "MESSAGE": "OK", + "DATA": { + "readonly": + "False" + } + }, + + "switches_sync_status_in_sync_resp": + { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://10.122.84.66:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/unit-test/inventory/switchesByFabric", + "MESSAGE": "OK", + "DATA": [ + { + "ipAddress": "172.31.217.101", + "ccStatus": "In-Sync" + }, + { + "ipAddress": "172.31.217.103", + "ccStatus": "In-Sync" + }, + { + "ipAddress": "10.122.84.174", + "ccStatus": "In-Sync" + }, + { + "ipAddress": "10.122.84.175", + "ccStatus": "In-Sync" + }] + }, + + "switches_sync_status_not_in_sync_resp": + { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://10.122.84.66:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/unit-test/inventory/switchesByFabric", + "MESSAGE": "OK", + "DATA": [ + { + "ipAddress": "172.31.217.101", + "ccStatus": "Not-In-Sync" + }, + { + "ipAddress": "172.31.217.103", + "ccStatus": "Not-In-Sync" + }, + { + "ipAddress": "10.122.84.174", + "ccStatus": "Not-In-Sync" + }, + { + "ipAddress": "10.122.84.175", + "ccStatus": "Not-In-Sync" + }] + }, + + "config_save_succ_resp": + { + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://10.122.84.66:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/unit-test/config-save", + "MESSAGE": "OK", + "DATA": { + "status": "Config save is completed" + } + }, + + "config_save_fail_resp": + { + "RETURN_CODE": 500, + "METHOD": "POST", + "REQUEST_PATH": "https://10.122.84.66:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/unit-test/config-save", + "MESSAGE": "", + "DATA": { + "status": "" + } + }, + + "fabric_inventory_details_resp": + { + "172.31.217.101": { + "switchRoleEnum": "Leaf", + "fabricTechnology": "LANClassic", + "ipAddress": "172.31.217.101", + "fabricName": "unit-test", + "logicalName": "dt-n9k1", + "switchDbID": 777660, + "serialNumber": "FDO24020JMB", + "managable": true + }, + "172.31.217.103": { + "switchRoleEnum": "Leaf", + "fabricTechnology": "LANClassic", + "ipAddress": "172.31.217.103", + "fabricName": "unit-test", + "logicalName": "dt-n9k1", + "switchDbID": 777660, + "serialNumber": "FDO24020JMT", + "managable": true + }, + "10.122.84.174": { + "switchRoleEnum": "Leaf", + "fabricTechnology": "LANClassic", + "ipAddress": "10.122.84.174", + "fabricName": "unit-test", + "logicalName": "dt-n9k1", + "switchDbID": 777660, + "serialNumber": "SAL1820SDPP", + "managable": true + }, + "10.122.84.175": { + "switchRoleEnum": "Leaf", + "fabricTechnology": "LANClassic", + "ipAddress": "10.122.84.175", + "fabricName": "unit-test", + "logicalName": "dt-n9k2-1", + "switchDbID": 777620, + "serialNumber": "SAL1819S6K3", + "managable": true + } + } +} diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_sgrp_association/dcnm_sgrp_association_common.py b/tests/unit/modules/dcnm/fixtures/dcnm_sgrp_association/dcnm_sgrp_association_common.py new file mode 100644 index 000000000..8fb5d26c6 --- /dev/null +++ b/tests/unit/modules/dcnm/fixtures/dcnm_sgrp_association/dcnm_sgrp_association_common.py @@ -0,0 +1,56 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import ( + AnsibleFailJson, +) + +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_sgrp_association import ( + DcnmSgrpAssociation, +) + + +class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + + params = { + "config": [], + "state": "merged", + "fabric": "mmudigon", + "deploy": True, + } + supports_check_mode = True + + @staticmethod + def fail_json(msg, **kwargs) -> AnsibleFailJson: + """ + mock the fail_json method + """ + raise AnsibleFailJson(msg, kwargs) + + +@pytest.fixture(name="dcnm_sgrp_association_fixture") +def dcnm_sgrp_association_fixture(monkeypatch): + """ + mock DcnmSgrpAssociation + """ + + return DcnmSgrpAssociation(MockAnsibleModule) diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_sgrp_association/dcnm_sgrp_association_config.json b/tests/unit/modules/dcnm/fixtures/dcnm_sgrp_association/dcnm_sgrp_association_config.json new file mode 100644 index 000000000..0f16d4e1e --- /dev/null +++ b/tests/unit/modules/dcnm/fixtures/dcnm_sgrp_association/dcnm_sgrp_association_config.json @@ -0,0 +1,56 @@ +{ + "sgrp_assoc_1_2": + { + "src_group_id": 15001, + "dst_group_id": 15002, + "src_group_name": "LSG_15001", + "dst_group_name": "LSG_15002", + "vrf_name": "VRF_12", + "contract_name": "CONTRACT12", + "switch": ["172.31.217.103", "172.31.217.101"] + }, + + "sgrp_assoc_1_3": + { + "src_group_id": 15001, + "dst_group_id": 15003, + "src_group_name": "LSG_15001", + "dst_group_name": "LSG_15003", + "vrf_name": "VRF_13", + "contract_name": "CONTRACT13", + "switch": ["172.31.217.103", "172.31.217.101"] + }, + + "sgrp_assoc_1_4": + { + "src_group_id": 15001, + "dst_group_id": 15004, + "src_group_name": "LSG_15001", + "dst_group_name": "LSG_15004", + "vrf_name": "VRF_14", + "contract_name": "CONTRACT14", + "switch": ["172.31.217.103", "172.31.217.101"] + }, + + "sgrp_assoc_2_3": + { + "src_group_id": 15002, + "dst_group_id": 15003, + "src_group_name": "LSG_15002", + "dst_group_name": "LSG_15003", + "vrf_name": "VRF_23", + "contract_name": "CONTRACT23", + "switch": ["172.31.217.103", "172.31.217.101"] + }, + + "sgrp_assoc_2_4": + { + "src_group_id": 15002, + "dst_group_id": 15004, + "src_group_name": "LSG_15002", + "dst_group_name": "LSG_15004", + "vrf_name": "VRF_24", + "contract_name": "CONTRACT24", + "switch": ["172.31.217.103", "172.31.217.101"] + } +} diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_sgrp_association/dcnm_sgrp_association_data.json b/tests/unit/modules/dcnm/fixtures/dcnm_sgrp_association/dcnm_sgrp_association_data.json new file mode 100644 index 000000000..1e69ea031 --- /dev/null +++ b/tests/unit/modules/dcnm/fixtures/dcnm_sgrp_association/dcnm_sgrp_association_data.json @@ -0,0 +1,126 @@ +{ + "sgrp_assoc_have_1_2": + { + "uuid": "15001-15002", + "fabricName": "unit-test", + "vrfName": "VRF_12", + "srcGroupId": 15001, + "dstGroupId": 15002, + "srcGroupName": "LSG_15001", + "dstGroupName": "LSG_15002", + "contractName": "CONTRACT12", + "status": "PENDING" + }, + + "sgrp_assoc_have_1_3": + { + "uuid": "15001-15003", + "fabricName": "unit-test", + "vrfName": "VRF_13", + "srcGroupId": 15001, + "dstGroupId": 15003, + "srcGroupName": "LSG_15001", + "dstGroupName": "LSG_15003", + "contractName": "CONTRACT13", + "status": "PENDING" + }, + + "sgrp_assoc_have_1_4": + { + "uuid": "15001-15004", + "fabricName": "unit-test", + "vrfName": "VRF_14", + "srcGroupId": 15001, + "dstGroupId": 15004, + "srcGroupName": "LSG_15001", + "dstGroupName": "LSG_15004", + "contractName": "CONTRACT14", + "status": "PENDING" + }, + + "sgrp_assoc_have_2_3": + { + "uuid": "15002-15003", + "fabricName": "unit-test", + "vrfName": "VRF_23", + "srcGroupId": 15002, + "dstGroupId": 15003, + "srcGroupName": "LSG_15002", + "dstGroupName": "LSG_15003", + "contractName": "CONTRACT23", + "status": "PENDING" + }, + + "security_group_1": + { + "fabricName": "unit-test", + "groupId": 15001, + "groupName": "LSG_15001", + "groupType": "", + "ipSelectors": [ + ], + "networkSelectors": [ + ] + }, + + "security_group_2": + { + "fabricName": "unit-test", + "groupId": 15002, + "groupName": "LSG_15002", + "groupType": "", + "ipSelectors": [ + ], + "networkSelectors": [ + ] + }, + + "security_group_3": + { + "fabricName": "unit-test", + "groupId": 15003, + "groupName": "LSG_15003", + "groupType": "", + "ipSelectors": [ + ], + "networkSelectors": [ + ] + }, + + "security_group_4": + { + "fabricName": "unit-test", + "groupId": 15004, + "groupName": "LSG_15004", + "groupType": "", + "ipSelectors": [ + ], + "networkSelectors": [ + ] + }, + + "security_group_5": + { + "fabricName": "unit-test", + "groupId": 15005, + "groupName": "LSG_15005", + "groupType": "", + "ipSelectors": [ + ], + "networkSelectors": [ + ] + }, + + "managable_switches": + { + "172.31.217.103": "FDO24020JMT", + "172.31.217.101": "FDO24020JMB", + "93180YC-FX3S-L1-S1": "FDO24020JMT", + "93180YC-FX3S-L2-S1": "FDO24020JMB" + }, + + "meta_switches": [ + ["172.31.217.103", "93180YC-FX3S-L1-S1", "FDO24020JMT"], + ["172.31.217.101", "93180YC-FX3S-L2-S1", "FDO24020JMB"] + ] +} diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_sgrp_association/dcnm_sgrp_association_response.json b/tests/unit/modules/dcnm/fixtures/dcnm_sgrp_association/dcnm_sgrp_association_response.json new file mode 100644 index 000000000..74a382705 --- /dev/null +++ b/tests/unit/modules/dcnm/fixtures/dcnm_sgrp_association/dcnm_sgrp_association_response.json @@ -0,0 +1,57 @@ +{ + "security_groups_all_resp": + { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://10.78.210.227:443/appcenter/cisco/ndfc/api/v1/security/fabrics/unit-test/groups", + "MESSAGE": "OK", + "DATA": [ + ] + }, + + "sgrp_assoc_all_resp": + { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://10.78.210.227:443/appcenter/cisco/ndfc/api/v1/security/fabrics/unit-test/contractAssociations", + "MESSAGE": "OK", + "DATA": [ + ] + }, + + "sgrp_assoc_deploy_resp": + { + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://10.78.210.227:443/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/unit-test/config-deploy/FDO24020JMT,FDO24020JMB?forceShowRun=false", + "MESSAGE": "OK", + "DATA": {"status": "Configuration deployment completed."} + }, + + "sgrp_assoc_create_resp": + { + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://10.78.210.227:443/appcenter/cisco/ndfc/api/v1/security/fabrics/unit-test/contractAssociations", + "MESSAGE": "OK", + "DATA": {} + }, + + "sgrp_assoc_delete_resp": + { + "RETURN_CODE": 200, + "METHOD": "POST", + "REQUEST_PATH": "https://10.78.210.227:443/appcenter/cisco/ndfc/api/v1/security/fabrics/unit-test/contractAssociations/bulkDelete", + "MESSAGE": "OK", + "DATA": {} + }, + + "sgrp_assoc_modify_resp": + { + "RETURN_CODE": 200, + "METHOD": "PUT", + "REQUEST_PATH": "https://10.78.210.227:443/appcenter/cisco/ndfc/api/v1/security/fabrics/unit-test/contractAssociations", + "MESSAGE": "OK", + "DATA": {} + } +} diff --git a/tests/unit/modules/dcnm/test_dcnm_sgrp_associtaion.py b/tests/unit/modules/dcnm/test_dcnm_sgrp_associtaion.py new file mode 100644 index 000000000..aad5e32cc --- /dev/null +++ b/tests/unit/modules/dcnm/test_dcnm_sgrp_associtaion.py @@ -0,0 +1,3140 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# See the following regarding *_fixture imports +# https://pylint.pycqa.org/en/latest/user_guide/messages/warning/redefined-outer-name.html +# Due to the above, we also need to disable unused-import +# pylint: disable=unused-import +# Some fixtures need to use *args to match the signature of the function they are mocking +# pylint: disable=unused-argument +# Some tests require calling protected methods +# pylint: disable=protected-access + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +__copyright__ = "Copyright (c) 2024 Cisco and/or its affiliates." +__author__ = "Mallik Mudigonda" + +from unittest.mock import patch +from _pytest.monkeypatch import MonkeyPatch + +from .dcnm_module import TestDcnmModule, set_module_args, loadPlaybookData + +# from typing import Any, Dict + +import os +import copy +import json +import pytest + +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm import ( + dcnm_sgrp_association_utils, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm import ( + dcnm, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common import ( + sender_dcnm, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm_sgrp_association_utils import ( + dcnm_sgrp_association_paths as sgrp_association_paths, + Paths, + dcnm_sgrp_association_utils_check_if_meta, + dcnm_sgrp_association_utils_validate_devices, + dcnm_sgrp_association_utils_get_paths, +) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.common_utils import ( + Version, + InventoryData, + FabricInfo, + SwitchInfo, +) + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.rest_send_v2 import ( + RestSend, +) + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import ( + ResponseHandler, +) + +from ansible_collections.cisco.dcnm.plugins.module_utils.common.sender_dcnm import ( + Sender, +) + +from ansible_collections.ansible.netcommon.tests.unit.modules.utils import ( + AnsibleFailJson, +) +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_sgrp_association import ( + DcnmSgrpAssociation, +) +from ansible_collections.cisco.dcnm.plugins.modules import ( + dcnm_sgrp_association, +) + +from ansible_collections.cisco.dcnm.plugins.module_utils.common import ( + common_utils, +) + +# Importing Fixtures +from .fixtures.dcnm_sgrp_association.dcnm_sgrp_association_common import ( + dcnm_sgrp_association_fixture, +) + +from unittest.mock import Mock + +# Fixtures path +fixture_path = os.path.join(os.path.dirname(__file__), "fixtures") + +# UNIT TEST CASES + + +def load_data(module_name, module_dir): + + module_data_path = fixture_path + "/" + module_dir + path = os.path.join(module_data_path, "{0}.json".format(module_name)) + + with open(path) as f: + data = f.read() + + try: + j_data = json.loads(data) + except Exception as e: + pass + + return j_data + + +class TestDcnmSgrpAssociationModule(TestDcnmModule): + + module = dcnm_sgrp_association + + fd = None + + def setUp(self): + super(TestDcnmSgrpAssociationModule, self).setUp() + self.monkeypatch = MonkeyPatch() + + def dcnm_mock_version(self, value): + dcnm_version_supported_side_effect = [] + dcnm_version_supported_side_effect.append(value) + mock_dcnm_version_supported = Mock( + side_effect=dcnm_version_supported_side_effect + ) + self.monkeypatch.setattr( + common_utils, "dcnm_version_supported", mock_dcnm_version_supported + ) + + def dcnm_mock_fabric_inv_details(self, value): + get_fabric_inventory_details_side_effect = [] + get_fabric_inventory_details_side_effect.append(value) + mock_get_fabric_inventory_details = Mock( + side_effect=get_fabric_inventory_details_side_effect + ) + self.monkeypatch.setattr( + common_utils, + "get_fabric_inventory_details", + mock_get_fabric_inventory_details, + ) + + def dcnm_mock_dcnm_sender(self, value): + dcnm_sender_side_effect = [] + dcnm_sender_side_effect.append(value) + mock_dcnm_sender = Mock(side_effect=dcnm_sender_side_effect) + self.monkeypatch.setattr(sender_dcnm, "dcnm_send", mock_dcnm_sender) + + def dcnm_mock_dcnm_send(self, value): + dcnm_send_side_effect = [] + dcnm_send_side_effect.extend(value) + mock_dcnm_send = Mock(side_effect=dcnm_send_side_effect) + self.monkeypatch.setattr( + dcnm_sgrp_association_utils, "dcnm_send", mock_dcnm_send + ) + + def dcnm_load_required_files(self): + sgrp_assoc_resp = load_data( + "dcnm_sgrp_association_response", "dcnm_sgrp_association" + ) + sgrp_assoc_config = load_data( + "dcnm_sgrp_association_config", "dcnm_sgrp_association" + ) + sgrp_assoc_common = load_data("common_responses", "common") + sgrp_assoc_data = load_data( + "dcnm_sgrp_association_data", "dcnm_sgrp_association" + ) + + return ( + sgrp_assoc_resp, + sgrp_assoc_common, + sgrp_assoc_data, + sgrp_assoc_config, + ) + + def dcnm_mock_common_information(self, common_resp): + self.dcnm_mock_version(12) + self.dcnm_mock_fabric_inv_details( + common_resp.get("fabric_inventory_details_resp") + ) + self.dcnm_mock_dcnm_sender(common_resp.get("access_mode_resp")) + + def dcnm_assert_result_common_info(self, result, match_data): + self.assertEqual( + len(result["diff"][0]["merged"]), match_data["merged"] + ) + self.assertEqual( + len(result["diff"][0]["modified"]), match_data["modified"] + ) + self.assertEqual( + len(result["diff"][0]["deleted"]), match_data["deleted"] + ) + self.assertEqual(len(result["diff"][0]["query"]), match_data["query"]) + self.assertEqual( + len(result["diff"][0]["deploy"]), match_data["deploy"] + ) + + # Validate create responses + for resp in result["response"]: + self.assertEqual(resp["RETURN_CODE"], 200) + + def test_dcnm_sgrp_association_merged_new(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + [], + [], + [], + [], + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_3"), + sgrp_assoc_config.get("sgrp_assoc_1_4"), + sgrp_assoc_config.get("sgrp_assoc_2_3"), + ] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 4, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 1, + }, + ) + + def test_dcnm_sgrp_association_merged_new_without_group_ids(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_security_groups_resp = sgrp_assoc_resp.get( + "security_groups_all_resp" + ) + all_security_groups_resp["DATA"].append( + { + "groupName": "LSG_15001", + "groupId": 15001, + "ipSelectors": [], + "networkSelectors": [], + } + ) + all_security_groups_resp["DATA"].append( + { + "groupName": "LSG_15002", + "groupId": 15002, + "ipSelectors": [], + "networkSelectors": [], + } + ) + all_security_groups_resp["DATA"].append( + { + "groupName": "LSG_15003", + "groupId": 15003, + "ipSelectors": [], + "networkSelectors": [], + } + ) + all_security_groups_resp["DATA"].append( + { + "groupName": "LSG_15004", + "groupId": 15004, + "ipSelectors": [], + "networkSelectors": [], + } + ) + all_security_groups_resp["DATA"].append( + { + "groupName": "LSG_15005", + "groupId": 15005, + "ipSelectors": [], + "networkSelectors": [], + } + ) + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_security_groups_resp, + [], + [], + [], + [], + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + modified_12_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_12_config.pop("src_group_id") + modified_12_config.pop("dst_group_id") + + modified_14_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_4") + ) + modified_14_config.pop("src_group_id") + modified_14_config.pop("dst_group_id") + + # load required config data + playbook_config = [ + modified_12_config, + sgrp_assoc_config.get("sgrp_assoc_1_3"), + modified_14_config, + sgrp_assoc_config.get("sgrp_assoc_2_3"), + ] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 4, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 1, + }, + ) + + def test_dcnm_sgrp_association_merged_new_with_no_groups(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_security_groups_resp = sgrp_assoc_resp.get( + "security_groups_all_resp" + ) + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_security_groups_resp, + all_security_groups_resp, + [], + [], + [], + [], + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + modified_12_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_12_config.pop("src_group_id") + modified_12_config.pop("dst_group_id") + + modified_14_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_4") + ) + modified_14_config.pop("src_group_id") + modified_14_config.pop("dst_group_id") + + # load required config data + playbook_config = [ + modified_12_config, + sgrp_assoc_config.get("sgrp_assoc_1_3"), + modified_14_config, + sgrp_assoc_config.get("sgrp_assoc_2_3"), + ] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 4, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 1, + }, + ) + + def test_dcnm_sgrp_association_merged_new_deploy_fail(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + deploy_fail_resp = copy.deepcopy( + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp") + ) + deploy_fail_resp["RETURN_CODE"] = 500 + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + [], + [], + [], + [], + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + deploy_fail_resp, + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_3"), + sgrp_assoc_config.get("sgrp_assoc_1_4"), + sgrp_assoc_config.get("sgrp_assoc_2_3"), + ] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + try: + result = self.execute_module(changed=True, failed=False) + except Exception as e: + result = eval(str(e)) + assert result["msg"]["RETURN_CODE"] == 500 + + def test_dcnm_sgrp_association_merged_new_duplicate(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + [], + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_2"), + ] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 1, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 1, + }, + ) + + def test_dcnm_sgrp_association_merged_new_with_deploy_none(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + [], + [], + [], + [], + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_3"), + sgrp_assoc_config.get("sgrp_assoc_1_4"), + sgrp_assoc_config.get("sgrp_assoc_2_3"), + ] + + set_module_args( + dict( + deploy="none", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 4, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 0, + }, + ) + + def test_dcnm_sgrp_association_merged_new_without_deploy_flag(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + [], + [], + [], + [], + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_3"), + sgrp_assoc_config.get("sgrp_assoc_1_4"), + sgrp_assoc_config.get("sgrp_assoc_2_3"), + ] + + set_module_args( + dict(state="merged", fabric="unit-test", config=playbook_config) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 4, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 1, + }, + ) + + def test_dcnm_sgrp_association_merged_new_without_state(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + [], + [], + [], + [], + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_3"), + sgrp_assoc_config.get("sgrp_assoc_1_4"), + sgrp_assoc_config.get("sgrp_assoc_2_3"), + ] + + set_module_args( + dict(deploy="switches", fabric="unit-test", config=playbook_config) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 4, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 1, + }, + ) + + def test_dcnm_sgrp_association_merged_idempotence(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_3"), + sgrp_assoc_config.get("sgrp_assoc_1_4"), + sgrp_assoc_config.get("sgrp_assoc_2_3"), + ] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 0, + }, + ) + + def test_dcnm_sgrp_association_merged_existing(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + modified_config_12 = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_config_12["contract_name"] = "MODIFIED_CONTRACT1" + modified_config_13 = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_3") + ) + modified_config_13["contract_name"] = "MODIFIED_CONTRACT2" + modified_config_14 = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_4") + ) + modified_config_14["contract_name"] = "MODIFIED_CONTRACT3" + modified_config_23 = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_2_3") + ) + modified_config_23["contract_name"] = "MODIFIED_CONTRACT4" + + # load required config data + playbook_config = [ + modified_config_12, + modified_config_13, + modified_config_14, + modified_config_23, + ] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 4, + "deleted": 0, + "query": 0, + "deploy": 1, + }, + ) + + def test_dcnm_sgrp_association_merged_existing_duplicate(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + modified_config_12 = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_config_12["contract_name"] = "MODIFIED_CONTRACT1" + + # load required config data + playbook_config = [modified_config_12, modified_config_12] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 1, + "deleted": 0, + "query": 0, + "deploy": 1, + }, + ) + + def test_dcnm_sgrp_association_merged_existing_create_fail(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + create_fail_resp = copy.deepcopy( + sgrp_assoc_resp.get("sgrp_assoc_create_resp") + ) + create_fail_resp["RETURN_CODE"] = 500 + create_fail_resp["MESSAGE"] = "" + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + create_fail_resp, + create_fail_resp, + create_fail_resp, + create_fail_resp, + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + modified_config_12 = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_config_12["contract_name"] = "MODIFIED_CONTRACT1" + modified_config_13 = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_3") + ) + modified_config_13["contract_name"] = "MODIFIED_CONTRACT2" + modified_config_14 = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_4") + ) + modified_config_14["contract_name"] = "MODIFIED_CONTRACT3" + modified_config_23 = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_2_3") + ) + modified_config_23["contract_name"] = "MODIFIED_CONTRACT4" + + # load required config data + playbook_config = [ + modified_config_12, + modified_config_13, + modified_config_14, + modified_config_23, + ] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + try: + result = self.execute_module(changed=True, failed=False) + except Exception as e: + result = eval(str(e)) + assert result["msg"]["RETURN_CODE"] == 500 + + def test_dcnm_sgrp_association_merged_new_create_fail(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + create_fail_resp = copy.deepcopy( + sgrp_assoc_resp.get("sgrp_assoc_create_resp") + ) + create_fail_resp["RETURN_CODE"] = 500 + create_fail_resp["MESSAGE"] = "" + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + [], + [], + [], + [], + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + create_fail_resp, + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_3"), + sgrp_assoc_config.get("sgrp_assoc_1_4"), + sgrp_assoc_config.get("sgrp_assoc_2_3"), + ] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + try: + result = self.execute_module(changed=True, failed=False) + except Exception as e: + result = eval(str(e)) + assert result["msg"]["RETURN_CODE"] == 500 + + def test_dcnm_sgrp_association_merged_existing_and_non_existing(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_3"), + sgrp_assoc_config.get("sgrp_assoc_1_4"), + sgrp_assoc_config.get("sgrp_assoc_2_3"), + ] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 2, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 1, + }, + ) + + def test_dcnm_sgrp_association_merged_no_group_ids(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_security_groups = sgrp_assoc_resp.get("security_groups_all_resp") + all_security_groups["DATA"] = [ + sgrp_assoc_data.get("security_group_1"), + sgrp_assoc_data.get("security_group_2"), + sgrp_assoc_data.get("security_group_3"), + sgrp_assoc_data.get("security_group_4"), + sgrp_assoc_data.get("security_group_5"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_security_groups, + [], + [], + [], + [], + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + modified_13_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_3") + ) + modified_23_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_2_3") + ) + modified_13_config.pop("src_group_id") + modified_13_config.pop("dst_group_id") + modified_23_config.pop("src_group_id") + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + modified_13_config, + sgrp_assoc_config.get("sgrp_assoc_1_4"), + sgrp_assoc_config.get("sgrp_assoc_2_3"), + modified_23_config, + ] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 4, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 1, + }, + ) + + def test_dcnm_sgrp_association_merged_deploy_retry(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + self.dcnm_mock_dcnm_send( + [ + [], + [], + [], + [], + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_3"), + sgrp_assoc_config.get("sgrp_assoc_1_4"), + sgrp_assoc_config.get("sgrp_assoc_2_3"), + ] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 4, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 1, + }, + ) + + def test_dcnm_sgrp_association_merged_deploy_retry_fail(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + self.dcnm_mock_dcnm_send( + [ + [], + [], + [], + [], + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_3"), + sgrp_assoc_config.get("sgrp_assoc_1_4"), + sgrp_assoc_config.get("sgrp_assoc_2_3"), + ] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + try: + result = self.execute_module(changed=True, failed=False) + except Exception as e: + assert "did not reach 'In-Sync' state after deploy" in str(e) + + def test_dcnm_sgrp_association_merged_deploy_sync_status_fail(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + sync_status_fail_resp = copy.deepcopy( + sgrp_assoc_common.get("switches_sync_status_not_in_sync_resp") + ) + sync_status_fail_resp["RETURN_CODE"] = 500 + + self.dcnm_mock_dcnm_send( + [ + [], + [], + [], + [], + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sync_status_fail_resp, + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_3"), + sgrp_assoc_config.get("sgrp_assoc_1_4"), + sgrp_assoc_config.get("sgrp_assoc_2_3"), + ] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + try: + result = self.execute_module(changed=True, failed=False) + except Exception as e: + result = eval(str(e)) + assert result["msg"]["RETURN_CODE"] == 500 + + def test_dcnm_sgrp_association_merged_no_config(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + set_module_args( + dict(deploy="switches", state="merged", fabric="unit-test") + ) + + try: + result = self.execute_module(changed=False, failed=False) + except Exception as e: + assert "'config' element is mandatory for state 'merged'" in str(e) + + def test_dcnm_sgrp_association_merged_missing_src_group_name(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + modified_12_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_12_config.pop("src_group_name") + + # load required config data + playbook_config = [modified_12_config] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + try: + result = self.execute_module(changed=False, failed=False) + except Exception as e: + assert "'src_group_name : Required parameter not found'" in str(e) + + def test_dcnm_sgrp_association_merged_missing_dst_group_name(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + modified_12_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_12_config.pop("dst_group_name") + + # load required config data + playbook_config = [modified_12_config] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + try: + result = self.execute_module(changed=False, failed=False) + except Exception as e: + assert "'dst_group_name : Required parameter not found'" in str(e) + + def test_dcnm_sgrp_association_merged_missing_vrf_name(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + modified_12_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_12_config.pop("vrf_name") + + # load required config data + playbook_config = [modified_12_config] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + try: + result = self.execute_module(changed=False, failed=False) + except Exception as e: + assert "'vrf_name : Required parameter not found'" in str(e) + + def test_dcnm_sgrp_association_merged_missing_contract_name(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + modified_12_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_12_config.pop("contract_name") + + # load required config data + playbook_config = [modified_12_config] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + try: + result = self.execute_module(changed=False, failed=False) + except Exception as e: + assert "'contract_name : Required parameter not found'" in str(e) + + def test_dcnm_sgrp_association_merged_missing_switches(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + modified_12_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_12_config.pop("switch") + + # load required config data + playbook_config = [modified_12_config] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + try: + result = self.execute_module(changed=False, failed=False) + except Exception as e: + assert "'switch : Required parameter not found'" in str(e) + + def test_dcnm_sgrp_association_delete_existing_with_null_cfg(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_resp.get("sgrp_assoc_delete_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [] + + set_module_args( + dict( + deploy="switches", + state="deleted", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 0, + "deleted": 4, + "query": 0, + "deploy": 0, + }, + ) + assert len(result["diff"][0]["delete_deploy"]) == 4 + + def test_dcnm_sgrp_association_delete_existing_with_null_cfg_and_no_associations( + self + ): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + [], + [], + [], + [], + sgrp_assoc_resp.get("sgrp_assoc_delete_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [] + + set_module_args( + dict( + deploy="switches", + state="deleted", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 0, + }, + ) + assert len(result["diff"][0]["delete_deploy"]) == 0 + + def test_dcnm_sgrp_association_delete_existing_with_no_deploy(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_resp.get("sgrp_assoc_delete_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [] + + set_module_args( + dict( + deploy="none", + state="deleted", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 0, + "deleted": 4, + "query": 0, + "deploy": 0, + }, + ) + assert len(result["diff"][0]["delete_deploy"]) == 0 + + def test_dcnm_sgrp_association_delete_existing_fail(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + delete_fail_resp = copy.deepcopy( + sgrp_assoc_resp.get("sgrp_assoc_delete_resp") + ) + delete_fail_resp["RETURN_CODE"] = 500 + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + delete_fail_resp, + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [] + + set_module_args( + dict( + deploy="switches", + state="deleted", + fabric="unit-test", + config=playbook_config, + ) + ) + + try: + result = self.execute_module(changed=True, failed=False) + except Exception as e: + result = eval(str(e)) + assert result["msg"]["RETURN_CODE"] == 500 + + def test_dcnm_sgrp_association_delete_existing_duplicate(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_resp.get("sgrp_assoc_delete_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_2"), + ] + + set_module_args( + dict( + deploy="switches", + state="deleted", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 0, + "deleted": 1, + "query": 0, + "deploy": 0, + }, + ) + + def test_dcnm_sgrp_association_delete_existing_invalid_params(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_resp.get("sgrp_assoc_delete_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + modified_12_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_12_config["src_group_id"] = 10 + + # load required config data + playbook_config = [modified_12_config] + + set_module_args( + dict( + deploy="switches", + state="deleted", + fabric="unit-test", + config=playbook_config, + ) + ) + + try: + result = self.execute_module(changed=True, failed=False) + except Exception as e: + assert "Invalid parameters in playbook" in str(e) + assert "The item exceeds the allowed range" in str(e) + + def test_dcnm_sgrp_association_delete_existing_with_group_name(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_resp.get("sgrp_assoc_delete_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + modified_12_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_12_config.pop("vrf_name") + modified_12_config.pop("contract_name") + modified_12_config.pop("src_group_name") + modified_12_config.pop("src_group_id") + modified_12_config.pop("dst_group_id") + + # load required config data + playbook_config = [modified_12_config] + + set_module_args( + dict( + deploy="switches", + state="deleted", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 0, + "deleted": 1, + "query": 0, + "deploy": 0, + }, + ) + + def test_dcnm_sgrp_association_delete_existing_with_group_id(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_resp.get("sgrp_assoc_delete_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + modified_12_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_12_config.pop("src_group_id") + + # load required config data + playbook_config = [modified_12_config] + + set_module_args( + dict( + deploy="switches", + state="deleted", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 0, + "deleted": 1, + "query": 0, + "deploy": 0, + }, + ) + + def test_dcnm_sgrp_association_delete_existing_with_vrf_name(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_resp.get("sgrp_assoc_delete_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + modified_12_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_12_config.pop("src_group_id") + modified_12_config.pop("dst_group_id") + modified_12_config.pop("src_group_name") + modified_12_config.pop("dst_group_name") + modified_12_config.pop("contract_name") + + # load required config data + playbook_config = [modified_12_config] + + set_module_args( + dict( + deploy="switches", + state="deleted", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 0, + "deleted": 1, + "query": 0, + "deploy": 0, + }, + ) + + def test_dcnm_sgrp_association_delete_existing_with_contract_name(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_resp.get("sgrp_assoc_delete_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + modified_12_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_12_config.pop("src_group_id") + modified_12_config.pop("dst_group_id") + modified_12_config.pop("src_group_name") + modified_12_config.pop("dst_group_name") + modified_12_config.pop("vrf_name") + + # load required config data + playbook_config = [modified_12_config] + + set_module_args( + dict( + deploy="switches", + state="deleted", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 0, + "deleted": 1, + "query": 0, + "deploy": 0, + }, + ) + assert len(result["diff"][0]["delete_deploy"]) == 4 + + def test_dcnm_sgrp_association_delete_existing_with_mismatching_group_id( + self + ): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_resp.get("sgrp_assoc_delete_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + modified_12_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_12_config["src_group_id"] = 100 + + # load required config data + playbook_config = [modified_12_config] + + set_module_args( + dict( + deploy="switches", + state="deleted", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 0, + }, + ) + + def test_dcnm_sgrp_association_delete_existing_with_mismatching_group_name( + self + ): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_resp.get("sgrp_assoc_delete_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + modified_12_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_12_config["dst_group_name"] = "no_such_name" + + # load required config data + playbook_config = [modified_12_config] + + set_module_args( + dict( + deploy="switches", + state="deleted", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 0, + }, + ) + + def test_dcnm_sgrp_association_delete_existing_with_mismatching_vrf_name( + self + ): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_resp.get("sgrp_assoc_delete_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + modified_12_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_12_config["vrf_name"] = "no_such_name" + + # load required config data + playbook_config = [modified_12_config] + + set_module_args( + dict( + deploy="switches", + state="deleted", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 0, + }, + ) + + def test_dcnm_sgrp_association_delete_non_existing(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + all_sgrp_assocs, + all_sgrp_assocs, + sgrp_assoc_resp.get("sgrp_assoc_delete_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_3"), + sgrp_assoc_config.get("sgrp_assoc_1_4"), + ] + + set_module_args( + dict( + deploy="switches", + state="deleted", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 0, + }, + ) + + def test_dcnm_sgrp_association_sync_status_empty(self): + pass + + def test_dcnm_sgrp_association_replace(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + modified_12_config = copy.deepcopy( + sgrp_assoc_config.get("sgrp_assoc_1_2") + ) + modified_12_config["contract_name"] = "CONTRACT12_MODIFIED" + + # load required config data + playbook_config = [modified_12_config] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 1, + "deleted": 0, + "query": 0, + "deploy": 1, + }, + ) + + def test_dcnm_sgrp_association_override_null_cfg(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_resp.get("sgrp_assoc_delete_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [] + + set_module_args( + dict( + deploy="switches", + state="overridden", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 0, + "deleted": 4, + "query": 0, + "deploy": 0, + }, + ) + + def test_dcnm_sgrp_association_override_new_cfg(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_delete_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_4"), + sgrp_assoc_config.get("sgrp_assoc_2_4"), + ] + + set_module_args( + dict( + deploy="switches", + state="overridden", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 2, + "modified": 0, + "deleted": 2, + "query": 0, + "deploy": 1, + }, + ) + + def test_dcnm_sgrp_association_override_modified_cfg(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_delete_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + modified_23_config = sgrp_assoc_config.get("sgrp_assoc_2_3") + modified_23_config["contract_name"] = "MODIFIED_CONTRACT23" + + # load required config data + playbook_config = [modified_23_config] + + set_module_args( + dict( + deploy="switches", + state="overridden", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 0, + "modified": 1, + "deleted": 3, + "query": 0, + "deploy": 1, + }, + ) + + def test_dcnm_sgrp_association_override_existing_and_new_cfg(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + all_sgrp_assocs, + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_delete_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_3"), + sgrp_assoc_config.get("sgrp_assoc_1_4"), + sgrp_assoc_config.get("sgrp_assoc_2_3"), + ] + + set_module_args( + dict( + deploy="switches", + state="overridden", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 1, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 1, + }, + ) + + def test_dcnm_sgrp_association_query_existing_without_any_existing_info( + self + ): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [] + + # entries for get_have + self.dcnm_mock_dcnm_send([all_sgrp_assocs]) + + # load required config data + playbook_config = [] + + set_module_args( + dict( + deploy="none", + state="query", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + assert len(result["response"]) == 0 + + def test_dcnm_sgrp_association_query_existing_without_cfg(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send([all_sgrp_assocs]) + + # load required config data + playbook_config = [] + + set_module_args( + dict( + deploy="none", + state="query", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + assert len(result["response"]) == 4 + + def test_dcnm_sgrp_association_query_existing_with_contract_name(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send([all_sgrp_assocs]) + + # load required config data + playbook_config = [{"contract_name": "CONTRACT12"}] + + set_module_args( + dict( + deploy="none", + state="query", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + assert len(result["response"]) == 1 + + def test_dcnm_sgrp_association_query_existing_with_src_group_name(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send([all_sgrp_assocs]) + + # load required config data + playbook_config = [ + {"src_group_name": "LSG_15001"}, + {"src_group_name": "LSG_15002"}, + ] + + set_module_args( + dict( + deploy="none", + state="query", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + assert len(result["response"]) == 4 + + def test_dcnm_sgrp_association_query_existing_with_dst_group_name(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send([all_sgrp_assocs]) + + # load required config data + playbook_config = [ + {"dst_group_name": "LSG_15003"}, + {"dst_group_name": "LSG_15002"}, + ] + + set_module_args( + dict( + deploy="none", + state="query", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + assert len(result["response"]) == 3 + + def test_dcnm_sgrp_association_query_existing_with_src_group_id(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send([all_sgrp_assocs]) + + # load required config data + playbook_config = [{"src_group_id": 15001}] + + set_module_args( + dict( + deploy="none", + state="query", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + assert len(result["response"]) == 3 + + def test_dcnm_sgrp_association_query_with_invalid_src_group_id(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send([all_sgrp_assocs]) + + # load required config data + playbook_config = [{"src_group_id": 10}] + + set_module_args( + dict( + deploy="none", + state="query", + fabric="unit-test", + config=playbook_config, + ) + ) + + try: + result = self.execute_module(changed=False, failed=False) + except Exception as e: + assert "Invalid parameters in playbook" in str(e) + assert "The item exceeds the allowed range" in str(e) + + def test_dcnm_sgrp_association_query_existing_with_dst_group_id(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send([all_sgrp_assocs]) + + # load required config data + playbook_config = [{"dst_group_id": 15003}] + + set_module_args( + dict( + deploy="none", + state="query", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + assert len(result["response"]) == 2 + + def test_dcnm_sgrp_association_query_existing_with_vrf_name(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send([all_sgrp_assocs]) + + # load required config data + playbook_config = [{"vrf_name": "VRF_12"}] + + set_module_args( + dict( + deploy="none", + state="query", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + assert len(result["response"]) == 1 + + def test_dcnm_sgrp_association_query_existing_with_mismatching_info(self): + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + all_sgrp_assocs = sgrp_assoc_resp.get("sgrp_assoc_all_resp") + all_sgrp_assocs["DATA"] = [ + sgrp_assoc_data.get("sgrp_assoc_have_1_2"), + sgrp_assoc_data.get("sgrp_assoc_have_1_3"), + sgrp_assoc_data.get("sgrp_assoc_have_1_4"), + sgrp_assoc_data.get("sgrp_assoc_have_2_3"), + ] + + # entries for get_have + self.dcnm_mock_dcnm_send([all_sgrp_assocs]) + + # load required config data + playbook_config = [ + {"src_group_name": "LSG_15003", "vrf_name": "VRF_12"}, + {"dst_group_name": "LSG_15003", "vrf_name": "VRF_12"}, + {"src_group_id": 15002, "src_group_name": "LSG_15001"}, + {"src_group_id": 15002, "dst_group_name": "LSG_15004"}, + ] + + set_module_args( + dict( + deploy="none", + state="query", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + assert len(result["response"]) == 0 + + def test_dcnm_sgrp_association_verify_paths(self): + + try: + paths = Paths() + ver = paths.version + paths.commit() + except Exception as e: + assert ( + "Paths.commit(): version is not set, which is required" + in str(e) + ) + + def test_dcnm_sgrp_association_merged_new_check_mode(self): + + # Load the required files + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + [], + [], + [], + [], + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_3"), + sgrp_assoc_config.get("sgrp_assoc_1_4"), + sgrp_assoc_config.get("sgrp_assoc_2_3"), + ] + + set_module_args( + dict( + _ansible_check_mode=True, + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + self.dcnm_assert_result_common_info( + result, + { + "merged": 4, + "modified": 0, + "deleted": 0, + "query": 0, + "deploy": 1, + }, + ) + + def test_dcnm_sgrp_association_merged_new_with_meta_switches(self): + + # Load the required files + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + self.dcnm_load_required_files() + ) + + self.dcnm_mock_common_information(sgrp_assoc_common) + modified_inv_detail_resp = sgrp_assoc_common.get( + "fabric_inventory_details_resp" + ) + keys = list(modified_inv_detail_resp.keys()) + print(f"KEYS = {keys}\n") + modified_inv_detail_resp[keys[0]]["switchRoleEnum"] = None + self.dcnm_mock_fabric_inv_details(modified_inv_detail_resp) + + # entries for get_have + self.dcnm_mock_dcnm_send( + [ + [], + [], + [], + [], + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + sgrp_assoc_resp.get("sgrp_assoc_create_resp"), + sgrp_assoc_resp.get("sgrp_assoc_deploy_resp"), + sgrp_assoc_common.get("switches_sync_status_in_sync_resp"), + ] + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_3"), + sgrp_assoc_config.get("sgrp_assoc_1_4"), + sgrp_assoc_config.get("sgrp_assoc_2_3"), + ] + + set_module_args( + dict( + deploy="switches", + state="merged", + fabric="unit-test", + config=playbook_config, + ) + ) + + try: + result = self.execute_module(changed=True, failed=False) + except Exception as e: + assert "is not Manageable" in str(e) + + +def test_dcnm_sgrp_association_check_if_meta(dcnm_sgrp_association_fixture): + + test_module = TestDcnmSgrpAssociationModule() + sgrp_association = dcnm_sgrp_association_fixture + + # Load the required files + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + test_module.dcnm_load_required_files() + ) + + sgrp_association.meta_switches = sgrp_assoc_data.get("meta_switches") + + resp = dcnm_sgrp_association_utils_check_if_meta( + sgrp_association, "172.31.217.103" + ) + + assert resp is True + + +def test_dcnm_sgrp_association_utils_validate_devices( + dcnm_sgrp_association_fixture +): + + test_module = TestDcnmSgrpAssociationModule() + sgrp_association = dcnm_sgrp_association_fixture + + # Load the required files + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + test_module.dcnm_load_required_files() + ) + + # load required config data + playbook_config = [ + sgrp_assoc_config.get("sgrp_assoc_1_2"), + sgrp_assoc_config.get("sgrp_assoc_1_3"), + sgrp_assoc_config.get("sgrp_assoc_1_4"), + sgrp_assoc_config.get("sgrp_assoc_2_3"), + ] + + sgrp_association.managable = sgrp_assoc_data.get("managable_switches") + sgrp_association.meta_switches = sgrp_assoc_data.get("meta_switches") + try: + dcnm_sgrp_association_utils_validate_devices( + sgrp_association, playbook_config[0] + ) + except Exception as e: + assert "is not Manageable" in str(e) + + sgrp_association.managable.pop("172.31.217.103") + try: + dcnm_sgrp_association_utils_validate_devices( + sgrp_association, playbook_config[0] + ) + except Exception as e: + assert "Switch 172.31.217.103 is not Manageable" in str(e) + + +def test_dcnm_sgrp_association_utils_validate_common_utils( + dcnm_sgrp_association_fixture +): + + test_module = TestDcnmSgrpAssociationModule() + sgrp_association = dcnm_sgrp_association_fixture + + sgrp_assoc_resp, sgrp_assoc_common, sgrp_assoc_data, sgrp_assoc_config = ( + test_module.dcnm_load_required_files() + ) + + version = Version() + inv_data = InventoryData() + fabric_info = FabricInfo() + switch_info = SwitchInfo() + + try: + module = version.module + version.commit() + except Exception as e: + assert "module is not set, which is required" in str(e) + + try: + module = inv_data.module + fabric = inv_data.fabric + inv_data.commit() + except Exception as e: + assert "module is not set, which is required" in str(e) + + try: + inv_data.module = sgrp_association + inv_data.commit() + except Exception as e: + assert "fabric is not set, which is required" in str(e) + + try: + module = fabric_info.module + fabric = fabric_info.fabric + paths = fabric_info.paths + rest_send = fabric_info.rest_send + fabric_info.commit() + except Exception as e: + assert "module is not set, which is required" in str(e) + + try: + fabric_info.module = sgrp_association + fabric_info.commit() + except Exception as e: + assert "fabric is not set, which is required" in str(e) + + try: + fabric_info.module = sgrp_association + fabric_info.fabric = "unit-test" + fabric_info.commit() + except Exception as e: + assert "rest_send is not set, which is required" in str(e) + + try: + fabric_info.module = sgrp_association + fabric_info.fabric = "unit-test" + fabric_info.rest_send = RestSend(sgrp_association.params) + fabric_info.commit() + except Exception as e: + assert "paths is not set, which is required" in str(e) + + modified_access_mode_resp = sgrp_assoc_common.get("access_mode_resp") + modified_access_mode_resp["DATA"]["readonly"] = True + test_module.monkeypatch = MonkeyPatch() + test_module.dcnm_mock_dcnm_sender(modified_access_mode_resp) + + sender = Sender() + sender.ansible_module = sgrp_association + fabric_info.rest_send = RestSend(sgrp_association.params) + fabric_info.rest_send.response_handler = ResponseHandler() + fabric_info.rest_send.sender = sender + try: + fabric_info.module = sgrp_association.module + fabric_info.fabric = "unit-test" + fabric_info.paths = dcnm_sgrp_association_utils_get_paths(12) + fabric_info.commit() + except Exception as e: + assert ( + "in Monitoring mode, No changes are allowed on the fabric" + in str(e) + ) + + try: + switch_info.commit() + except Exception as e: + assert "nventory data is not set, which is required" in str(e) From b55d2731c44af418d0aead4de6dc9f995286b351 Mon Sep 17 00:00:00 2001 From: Mallik M J Date: Fri, 30 Aug 2024 16:16:08 +0530 Subject: [PATCH 2/6] Fixed an issue with Log --- plugins/modules/dcnm_sgrp_association.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/plugins/modules/dcnm_sgrp_association.py b/plugins/modules/dcnm_sgrp_association.py index f8a198552..742715b80 100644 --- a/plugins/modules/dcnm_sgrp_association.py +++ b/plugins/modules/dcnm_sgrp_association.py @@ -1012,17 +1012,28 @@ def main(): state = module.params["state"] - dcnm_sgrp_association.log.debug( - f"######################### BEGIN STATE = {0} ##########################\n".format(state) - ) - # Initialize the logger try: + # Set the following to True if logging is required + enable_logging = False logger = Log() + + if enable_logging is True: + collection_path = ( + "/Users/mmudigon/Desktop/Ansible/collections/ansible_collections/cisco/dcnm" + ) + config_file = ( + f"{collection_path}/plugins/module_utils/common/logging_config.json" + ) + logger.config = config_file logger.commit() except ValueError as error: module.fail_json(msg=str(error)) + dcnm_sgrp_association.log.debug( + f"######################### BEGIN STATE = {0} ##########################\n".format(state) + ) + # Initialize the Sender object sender = Sender() sender.ansible_module = module From df2ee14ca48b23814e2343887a0b8ca52f34fd90 Mon Sep 17 00:00:00 2001 From: Mallik M J Date: Fri, 30 Aug 2024 16:25:14 +0530 Subject: [PATCH 3/6] Fixed Sanity issues --- plugins/modules/dcnm_sgrp_association.py | 45 ++++++++++++++---------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/plugins/modules/dcnm_sgrp_association.py b/plugins/modules/dcnm_sgrp_association.py index 742715b80..5d04d09db 100644 --- a/plugins/modules/dcnm_sgrp_association.py +++ b/plugins/modules/dcnm_sgrp_association.py @@ -534,7 +534,9 @@ def dcnm_sgrp_association_get_diff_merge(self): ) self.log.info( - f"Compare Want and Have: Return Code = {0}, Reasons = {1}, Have = {2}\n".format(rc, reasons, have) + f"Compare Want and Have: Return Code = {0}, Reasons = {1}, Have = {2}\n".format( + rc, reasons, have + ) ) if rc == "DCNM_SGRP_ASSOCIATION_CREATE": @@ -931,8 +933,9 @@ def dcnm_sgrp_association_send_message_to_dcnm(self): ) self.log.debug( - f"Flags: CR = {0}, DL = {1}, MO = {2}, DP = {3}\n". - format(create_flag, delete_flag, modify_flag, deploy_flag) + f"Flags: CR = {0}, DL = {1}, MO = {2}, DP = {3}\n".format( + create_flag, delete_flag, modify_flag, deploy_flag + ) ) self.result["changed"] = ( @@ -1015,23 +1018,21 @@ def main(): # Initialize the logger try: # Set the following to True if logging is required - enable_logging = False + enable_logging = False logger = Log() - - if enable_logging is True: - collection_path = ( - "/Users/mmudigon/Desktop/Ansible/collections/ansible_collections/cisco/dcnm" - ) - config_file = ( - f"{collection_path}/plugins/module_utils/common/logging_config.json" - ) - logger.config = config_file + + if enable_logging is True: + collection_path = "/Users/mmudigon/Desktop/Ansible/collections/ansible_collections/cisco/dcnm" + config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" + logger.config = config_file logger.commit() except ValueError as error: module.fail_json(msg=str(error)) dcnm_sgrp_association.log.debug( - f"######################### BEGIN STATE = {0} ##########################\n".format(state) + f"######################### BEGIN STATE = {0} ##########################\n".format( + state + ) ) # Initialize the Sender object @@ -1066,7 +1067,9 @@ def main(): f"Config Info = {0}\n".format(dcnm_sgrp_association.config) ) dcnm_sgrp_association.log.info( - f"Validated Security Group Association Info = {0}\n".format(dcnm_sgrp_association.sgrp_association_info) + f"Validated Security Group Association Info = {0}\n".format( + dcnm_sgrp_association.sgrp_association_info + ) ) if ( @@ -1095,7 +1098,9 @@ def main(): ) dcnm_sgrp_association.log.info( - f"Security Groups Info = {0}\n".format(dcnm_sgrp_association.sgrp_info) + f"Security Groups Info = {0}\n".format( + dcnm_sgrp_association.sgrp_info + ) ) if (module.params["state"] == "merged") or ( module.params["state"] == "replaced" @@ -1126,7 +1131,9 @@ def main(): f"Deploy Info = {0}\n".format(dcnm_sgrp_association.diff_deploy) ) dcnm_sgrp_association.log.info( - f"Delete Deploy Info = {0}\n".format(dcnm_sgrp_association.diff_delete_deploy) + f"Delete Deploy Info = {0}\n".format( + dcnm_sgrp_association.diff_delete_deploy + ) ) dcnm_sgrp_association.result["diff"] = dcnm_sgrp_association.changed_dict @@ -1151,7 +1158,9 @@ def main(): dcnm_sgrp_association.dcnm_sgrp_association_send_message_to_dcnm() dcnm_sgrp_association.log.debug( - f"######################### END STATE = {0} ##########################\n".format(state) + f"######################### END STATE = {0} ##########################\n".format( + state + ) ) module.exit_json(**dcnm_sgrp_association.result) From e3ae0d7933a6be329025c5f0330a4d5a379433ea Mon Sep 17 00:00:00 2001 From: Mallik M J Date: Fri, 30 Aug 2024 16:51:00 +0530 Subject: [PATCH 4/6] Changed usage of Log to common.log --- plugins/modules/dcnm_sgrp_association.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/dcnm_sgrp_association.py b/plugins/modules/dcnm_sgrp_association.py index 5d04d09db..6b2faa833 100644 --- a/plugins/modules/dcnm_sgrp_association.py +++ b/plugins/modules/dcnm_sgrp_association.py @@ -317,7 +317,7 @@ import logging from ansible.module_utils.basic import AnsibleModule -from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import ( +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import ( Log, ) from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import ( @@ -1019,7 +1019,7 @@ def main(): try: # Set the following to True if logging is required enable_logging = False - logger = Log() + logger = Log(module) if enable_logging is True: collection_path = "/Users/mmudigon/Desktop/Ansible/collections/ansible_collections/cisco/dcnm" From 9ef4b84410905079dfa337143ddf26bd29abdf65 Mon Sep 17 00:00:00 2001 From: Mallik M J Date: Tue, 10 Sep 2024 16:02:31 +0530 Subject: [PATCH 5/6] A couple of fixes and updates --- .../dcnm/dcnm_sgrp_association_utils.py | 42 ++++--- plugins/modules/dcnm_sgrp_association.py | 105 +++++++----------- .../tests/dcnm/dcnm_sgrp_assoc_override.yaml | 19 +++- .../dcnm/test_dcnm_sgrp_associtaion.py | 6 +- 4 files changed, 89 insertions(+), 83 deletions(-) diff --git a/plugins/module_utils/network/dcnm/dcnm_sgrp_association_utils.py b/plugins/module_utils/network/dcnm/dcnm_sgrp_association_utils.py index 8c5892173..bde40133d 100644 --- a/plugins/module_utils/network/dcnm/dcnm_sgrp_association_utils.py +++ b/plugins/module_utils/network/dcnm/dcnm_sgrp_association_utils.py @@ -72,7 +72,9 @@ def commit(self): raise ValueError(msg) self.paths = dcnm_sgrp_association_utils_get_paths(self._version) - self.log.debug(f"Paths = {0}\n".format(self.paths)) + + msg = f"Paths = {self.paths}\n" + self.log.debug(msg) def dcnm_sgrp_association_utils_get_all_sgrp_info(self): @@ -92,7 +94,8 @@ def dcnm_sgrp_association_utils_get_all_sgrp_info(self): resp = dcnm_send(self.module, "GET", path) - self.log.info(f"DCNM:Get All Security Groups Resp = {resp}\n") + msg = f"DCNM:Get All Security Groups Resp = {resp}\n" + self.log.info(msg) if ( resp @@ -148,7 +151,8 @@ def dcnm_sgrp_association_utils_get_all_sgrp_association_info(self): resp = dcnm_send(self.module, "GET", path) - self.log.debug(f"DCNM:Get All SGRP Associations Resp = {0}\n".format(resp)) + msg = f"DCNM:Get All SGRP Associations Resp = {resp}\n" + self.log.debug(msg) if ( resp @@ -520,9 +524,8 @@ def dcnm_sgrp_association_utils_process_delete_payloads(self): resp = dcnm_send(self.module, "POST", path, json_payload) - self.log.info( - f"DCNM:Delete Path = {path}, Resp = {resp}. Payload = {json_payload}\n" - ) + msg = f"DCNM:Delete Path = {path}, Resp = {resp}, Payload = {json_payload}\n" + self.log.info(msg) if resp: self.result["response"].append(resp) @@ -563,9 +566,9 @@ def dcnm_sgrp_association_utils_process_payloads_list( # Creates can happen in bulk. json_payload = json.dumps(payload_list) resp = dcnm_send(self.module, command, path, json_payload) - self.log.info( - f"DCNM:Create Path = {path}, Resp = {resp}, Payload - {json_payload}\n" - ) + + msg = f"DCNM:Create Path = {path}, Resp = {resp}, Payload - {json_payload}\n" + self.log.info(msg) if resp: self.result["response"].append(resp) if resp and resp.get("RETURN_CODE") != 200: @@ -580,9 +583,9 @@ def dcnm_sgrp_association_utils_process_payloads_list( resp = dcnm_send( self.module, command, path + "/" + str(uuid), json_payload ) - self.log.info( - f"DCNM:Modify Path = {path}, Resp = {resp}, Payload = {json_payload}\n" - ) + + msg = f"DCNM:Modify Path = {path}, Resp = {resp}, Payload = {json_payload}\n" + self.log.info(msg) if resp: self.result["response"].append(resp) @@ -661,11 +664,15 @@ def dcnm_sgrp_association_utils_get_sync_status(self): resp = dcnm_send(self.module, "GET", path) - self.log.info(f"DCNM:Get Switch SYNC Status Resp = {resp}\n") + msg = f"DCNM:Get Switch SYNC Status Resp = {resp}\n" + self.log.info(msg) + status_info = [ {f"({d['ipAddress']} : {d['ccStatus']}"} for d in resp["DATA"] ] - self.log.info(f"Status Info = {status_info}\n") + + msg = f"Status Info = {status_info}\n" + self.log.info(msg) if resp and (resp["RETURN_CODE"] != 200): resp["CHANGED"] = self.changed_dict[0] @@ -706,8 +713,11 @@ def dcnm_sgrp_association_utils_deploy_payload(self, deploy_info): if resp: self.result["response"].append(resp) - self.log.info(f"DCNM:Deploy Element Path = {path}, Resp = {resp}\n") - self.log.info(f"Deploy Element Payload = {json_payload}\n") + msg = f"DCNM:Deploy Element Path = {path}, Resp = {resp}\n" + self.log.info(msg) + + msg = f"Deploy Element Payload = {json_payload}\n" + self.log.info(msg) if resp and (resp["RETURN_CODE"] != 200): resp["CHANGED"] = self.changed_dict[0] diff --git a/plugins/modules/dcnm_sgrp_association.py b/plugins/modules/dcnm_sgrp_association.py index 6b2faa833..3cd9e79d6 100644 --- a/plugins/modules/dcnm_sgrp_association.py +++ b/plugins/modules/dcnm_sgrp_association.py @@ -23,7 +23,7 @@ --- module: dcnm_sgrp_association short_description: DCNM Ansible Module for managing Security Groups Associatons. -version_added: "3.5.0" +version_added: "3.6.0" description: - "DCNM Ansible Module for managing Security Groups Associations." author: Mallik Mudigonda(@mmudigon) @@ -491,6 +491,8 @@ def dcnm_sgrp_association_get_diff_deleted(self): # is the contract_name to filter the output as required. if elem.get("contract_name", None) is not None: if have["contractName"] == elem["contract_name"]: + if elem.get("switch", None) is not None: + have["switch"] = elem["switch"] self.dcnm_sgrp_association_update_delete_payloads(have) else: self.dcnm_sgrp_association_update_delete_payloads(have) @@ -533,11 +535,8 @@ def dcnm_sgrp_association_get_diff_merge(self): self, elem ) - self.log.info( - f"Compare Want and Have: Return Code = {0}, Reasons = {1}, Have = {2}\n".format( - rc, reasons, have - ) - ) + msg = f"Compare Want and Have: Return Code = {rc}, Reasons = {reasons}, Have = {have}\n" + self.log.info(msg) if rc == "DCNM_SGRP_ASSOCIATION_CREATE": # Object does not exists, create a new one. @@ -692,6 +691,7 @@ def dcnm_sgrp_association_validate_deleted_state_input(self, cfg): "dst_group_name": {"type": "str"}, "contract_name": {"type": "str"}, "vrf_name": {"type": "str"}, + "switch": {"type": "list", "elements": "str"}, } sgrp_association_info, invalid_params = validate_list_of_dicts( @@ -932,11 +932,8 @@ def dcnm_sgrp_association_send_message_to_dcnm(self): self, self.diff_deploy ) - self.log.debug( - f"Flags: CR = {0}, DL = {1}, MO = {2}, DP = {3}\n".format( - create_flag, delete_flag, modify_flag, deploy_flag - ) - ) + msg = f"Flags: CR = {create_flag}, DL = {delete_flag}, MO = {modify_flag}, DP = {deploy_flag}\n" + self.log.debug(msg) self.result["changed"] = ( create_flag or modify_flag or delete_flag or deploy_flag @@ -1029,11 +1026,8 @@ def main(): except ValueError as error: module.fail_json(msg=str(error)) - dcnm_sgrp_association.log.debug( - f"######################### BEGIN STATE = {0} ##########################\n".format( - state - ) - ) + msg = f"######################### BEGIN STATE = {state} ##########################\n" + dcnm_sgrp_association.log.debug(msg) # Initialize the Sender object sender = Sender() @@ -1048,9 +1042,7 @@ def main(): if dcnm_sgrp_association.config == []: if state == "merged" or state == "replaced": module.fail_json( - msg="'config' element is mandatory for state '{0}', given = '{1}'".format( - state, dcnm_sgrp_association.config - ) + msg=f"'config' element is mandatory for state '{state}', given = '{dcnm_sgrp_association.config}'" ) dcnm_sgrp_association.dcnm_sgrp_association_update_switch_info() @@ -1063,14 +1055,11 @@ def main(): dcnm_sgrp_association.dcnm_sgrp_association_validate_all_input() - dcnm_sgrp_association.log.info( - f"Config Info = {0}\n".format(dcnm_sgrp_association.config) - ) - dcnm_sgrp_association.log.info( - f"Validated Security Group Association Info = {0}\n".format( - dcnm_sgrp_association.sgrp_association_info - ) - ) + msg = f"Config Info = {dcnm_sgrp_association.config}\n" + dcnm_sgrp_association.log.info(msg) + + msg = f"Validated Security Group Association Info = {dcnm_sgrp_association.sgrp_association_info}\n" + dcnm_sgrp_association.log.info(msg) if ( module.params["state"] != "query" @@ -1078,14 +1067,12 @@ def main(): ): dcnm_sgrp_association.dcnm_sgrp_association_get_want() - dcnm_sgrp_association.log.info( - f"Want = {0}\n".format(dcnm_sgrp_association.want) - ) + msg = f"Want = {dcnm_sgrp_association.want}\n" + dcnm_sgrp_association.log.info(msg) dcnm_sgrp_association.dcnm_sgrp_association_get_have() - dcnm_sgrp_association.log.info( - f"Have = {0}\n".format(dcnm_sgrp_association.have) - ) + msg = f"Have = {dcnm_sgrp_association.have}\n" + dcnm_sgrp_association.log.info(msg) # self.want would have defaulted all optional objects not included in playbook. But the way # these objects are handled is different between 'merged' and 'replaced' states. For 'merged' @@ -1093,15 +1080,13 @@ def main(): # they must be purged or defaulted. dcnm_sgrp_association.dcnm_sgrp_association_update_want() - dcnm_sgrp_association.log.info( - f"Updated Want = {0}\n".format(dcnm_sgrp_association.want) - ) - dcnm_sgrp_association.log.info( - f"Security Groups Info = {0}\n".format( - dcnm_sgrp_association.sgrp_info - ) - ) + msg = f"Updated Want = {dcnm_sgrp_association.want}\n" + dcnm_sgrp_association.log.info(msg) + + msg = f"Security Groups Info = {dcnm_sgrp_association.sgrp_info}\n" + dcnm_sgrp_association.log.info(msg) + if (module.params["state"] == "merged") or ( module.params["state"] == "replaced" ): @@ -1118,23 +1103,20 @@ def main(): if module.params["state"] == "query": dcnm_sgrp_association.dcnm_sgrp_association_get_diff_query() - dcnm_sgrp_association.log.info( - f"Create Info = {0}\n".format(dcnm_sgrp_association.diff_create) - ) - dcnm_sgrp_association.log.info( - f"Replace Info = {0}\n".format(dcnm_sgrp_association.diff_modify) - ) - dcnm_sgrp_association.log.info( - f"Delete Info = {0}\n".format(dcnm_sgrp_association.diff_delete) - ) - dcnm_sgrp_association.log.info( - f"Deploy Info = {0}\n".format(dcnm_sgrp_association.diff_deploy) - ) - dcnm_sgrp_association.log.info( - f"Delete Deploy Info = {0}\n".format( - dcnm_sgrp_association.diff_delete_deploy - ) - ) + msg = f"Create Info = {dcnm_sgrp_association.diff_create}\n" + dcnm_sgrp_association.log.info(msg) + + msg = f"Replace Info = {dcnm_sgrp_association.diff_modify}\n" + dcnm_sgrp_association.log.info(msg) + + msg = f"Delete Info = {dcnm_sgrp_association.diff_delete}\n" + dcnm_sgrp_association.log.info(msg) + + msg = f"Deploy Info = {dcnm_sgrp_association.diff_deploy}\n" + dcnm_sgrp_association.log.info(msg) + + msg = f"Delete Deploy Info = {dcnm_sgrp_association.diff_delete_deploy}\n" + dcnm_sgrp_association.log.info(msg) dcnm_sgrp_association.result["diff"] = dcnm_sgrp_association.changed_dict dcnm_sgrp_association.changed_dict[0]["debugs"].append( @@ -1157,11 +1139,8 @@ def main(): dcnm_sgrp_association.dcnm_sgrp_association_send_message_to_dcnm() - dcnm_sgrp_association.log.debug( - f"######################### END STATE = {0} ##########################\n".format( - state - ) - ) + msg = f"######################### END STATE = {state} ##########################\n" + dcnm_sgrp_association.log.debug(state) module.exit_json(**dcnm_sgrp_association.result) diff --git a/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_override.yaml b/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_override.yaml index 708e1625a..c48a5ea44 100644 --- a/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_override.yaml +++ b/tests/integration/targets/dcnm_sgrp_association/tests/dcnm/dcnm_sgrp_assoc_override.yaml @@ -126,7 +126,7 @@ ############################################## - name: Override Security Group Association with existing config modified - cisco.dcnm.dcnm_sgrp_association: + cisco.dcnm.dcnm_sgrp_association: &sgrp_assoc_over fabric: "{{ ansible_it_fabric }}" deploy: switches # choose from ["none", "switches"] state: overridden # choose from [merged, replaced, deleted, overridden, query] @@ -151,6 +151,23 @@ - '(result["diff"][0]["query"] | length) == 0' - '(result["diff"][0]["deploy"] | length) == 1' +############################################## +## OVERRIDE ## +############################################## + + - name: Override Security Group Association with existing config + cisco.dcnm.dcnm_sgrp_association: *sgrp_assoc_over + register: result + + - assert: + that: + - 'result.changed == false' + - '(result["diff"][0]["merged"] | length) == 0' + - '(result["diff"][0]["modified"] | length) == 0' + - '(result["diff"][0]["deleted"] | length) == 0' + - '(result["diff"][0]["query"] | length) == 0' + - '(result["diff"][0]["deploy"] | length) == 0' + ############################################## ## OVERRIDE ## ############################################## diff --git a/tests/unit/modules/dcnm/test_dcnm_sgrp_associtaion.py b/tests/unit/modules/dcnm/test_dcnm_sgrp_associtaion.py index aad5e32cc..fa925503b 100644 --- a/tests/unit/modules/dcnm/test_dcnm_sgrp_associtaion.py +++ b/tests/unit/modules/dcnm/test_dcnm_sgrp_associtaion.py @@ -78,6 +78,7 @@ from ansible_collections.ansible.netcommon.tests.unit.modules.utils import ( AnsibleFailJson, ) + from ansible_collections.cisco.dcnm.plugins.modules.dcnm_sgrp_association import ( DcnmSgrpAssociation, ) @@ -1999,7 +2000,7 @@ def test_dcnm_sgrp_association_delete_existing_with_contract_name(self): "deploy": 0, }, ) - assert len(result["diff"][0]["delete_deploy"]) == 4 + assert len(result["diff"][0]["delete_deploy"]) == 2 def test_dcnm_sgrp_association_delete_existing_with_mismatching_group_id( self @@ -2268,7 +2269,7 @@ def test_dcnm_sgrp_association_replace(self): set_module_args( dict( deploy="switches", - state="merged", + state="replaced", fabric="unit-test", config=playbook_config, ) @@ -2950,7 +2951,6 @@ def test_dcnm_sgrp_association_merged_new_with_meta_switches(self): "fabric_inventory_details_resp" ) keys = list(modified_inv_detail_resp.keys()) - print(f"KEYS = {keys}\n") modified_inv_detail_resp[keys[0]]["switchRoleEnum"] = None self.dcnm_mock_fabric_inv_details(modified_inv_detail_resp) From ff9b230d3bb9584b54a63a0502ff26458845b9e6 Mon Sep 17 00:00:00 2001 From: Praveen Ramoorthy Date: Tue, 12 Nov 2024 18:09:30 +0530 Subject: [PATCH 6/6] Logging support --- plugins/modules/dcnm_sgrp_association.py | 20 ++++++------------- tests/unit/module_utils/common/test_log_v2.py | 1 + 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/plugins/modules/dcnm_sgrp_association.py b/plugins/modules/dcnm_sgrp_association.py index 3cd9e79d6..23fd8b009 100644 --- a/plugins/modules/dcnm_sgrp_association.py +++ b/plugins/modules/dcnm_sgrp_association.py @@ -317,9 +317,8 @@ import logging from ansible.module_utils.basic import AnsibleModule -from ansible_collections.cisco.dcnm.plugins.module_utils.common.log import ( - Log, -) +from ansible_collections.cisco.dcnm.plugins.module_utils.common.log_v2 import Log + from ansible_collections.cisco.dcnm.plugins.module_utils.common.response_handler import ( ResponseHandler, ) @@ -1012,19 +1011,12 @@ def main(): state = module.params["state"] - # Initialize the logger + # Logging setup try: - # Set the following to True if logging is required - enable_logging = False - logger = Log(module) - - if enable_logging is True: - collection_path = "/Users/mmudigon/Desktop/Ansible/collections/ansible_collections/cisco/dcnm" - config_file = f"{collection_path}/plugins/module_utils/common/logging_config.json" - logger.config = config_file - logger.commit() + log = Log() + log.commit() except ValueError as error: - module.fail_json(msg=str(error)) + module.fail_json(str(error)) msg = f"######################### BEGIN STATE = {state} ##########################\n" dcnm_sgrp_association.log.debug(msg) diff --git a/tests/unit/module_utils/common/test_log_v2.py b/tests/unit/module_utils/common/test_log_v2.py index 120203855..9ebfbcb3d 100644 --- a/tests/unit/module_utils/common/test_log_v2.py +++ b/tests/unit/module_utils/common/test_log_v2.py @@ -406,6 +406,7 @@ def test_log_v2_00250(tmp_path) -> None: match += r"Error detail: Unable to configure handler.*" with pytest.raises(ValueError, match=match): instance.commit() + del environ['NDFC_LOGGING_CONFIG'] def test_log_v2_00300() -> None: