From 1caf1475a9e94e962c6f23ab05d208125d9d527e Mon Sep 17 00:00:00 2001 From: Praveen Ramoorthy Date: Mon, 9 Sep 2024 23:38:39 +0530 Subject: [PATCH 1/6] DCNM Security Groups Protocols and Contracts modules --- README.md | 4 +- docs/cisco.dcnm.dcnm_contracts_module.rst | 226 +++++ docs/cisco.dcnm.dcnm_protocols_module.rst | 476 ++++++++++ .../network/dcnm/dcnm_contracts_utils.py | 339 ++++++++ .../network/dcnm/dcnm_protocols_utils.py | 369 ++++++++ plugins/modules/dcnm_contracts.py | 751 ++++++++++++++++ plugins/modules/dcnm_protocols.py | 818 ++++++++++++++++++ tests/{config.yaml => config.yml} | 0 .../targets/dcnm_contracts/defaults/main.yaml | 2 + .../targets/dcnm_contracts/meta/main.yaml | 1 + .../targets/dcnm_contracts/tasks/dcnm.yaml | 32 + .../targets/dcnm_contracts/tasks/main.yaml | 70 ++ .../dcnm_contracts/tests/dcnm/deleted.yaml | 161 ++++ .../dcnm_contracts/tests/dcnm/merged.yaml | 255 ++++++ .../dcnm_contracts/tests/dcnm/overridden.yaml | 182 ++++ .../dcnm_contracts/tests/dcnm/query.yaml | 154 ++++ .../dcnm_contracts/tests/dcnm/replaced.yaml | 164 ++++ .../targets/dcnm_protocols/defaults/main.yaml | 2 + .../targets/dcnm_protocols/meta/main.yaml | 1 + .../targets/dcnm_protocols/tasks/dcnm.yaml | 24 + .../targets/dcnm_protocols/tasks/main.yaml | 47 + .../dcnm_protocols/tests/dcnm/deleted.yaml | 172 ++++ .../dcnm_protocols/tests/dcnm/merged.yaml | 260 ++++++ .../dcnm_protocols/tests/dcnm/overridden.yaml | 171 ++++ .../dcnm_protocols/tests/dcnm/query.yaml | 158 ++++ .../dcnm_protocols/tests/dcnm/replaced.yaml | 168 ++++ tests/sanity/ignore-2.10.txt | 2 + tests/sanity/ignore-2.11.txt | 2 + tests/sanity/ignore-2.12.txt | 2 + tests/sanity/ignore-2.13.txt | 2 + tests/sanity/ignore-2.14.txt | 2 + tests/sanity/ignore-2.15.txt | 2 + tests/sanity/ignore-2.16.txt | 2 + .../dcnm_contracts/dcnm_contracts_common.py | 55 ++ .../dcnm_contracts/dcnm_contracts_data.json | 44 + .../dcnm_contracts_response.json | 809 +++++++++++++++++ .../dcnm_protocols/dcnm_protocols_common.py | 55 ++ .../dcnm_protocols/dcnm_protocols_data.json | 45 + .../dcnm_protocols_response.json | 808 +++++++++++++++++ .../unit/modules/dcnm/test_dcnm_contracts.py | 656 ++++++++++++++ .../unit/modules/dcnm/test_dcnm_protocols.py | 527 +++++++++++ 41 files changed, 8019 insertions(+), 1 deletion(-) create mode 100644 docs/cisco.dcnm.dcnm_contracts_module.rst create mode 100644 docs/cisco.dcnm.dcnm_protocols_module.rst create mode 100644 plugins/module_utils/network/dcnm/dcnm_contracts_utils.py create mode 100644 plugins/module_utils/network/dcnm/dcnm_protocols_utils.py create mode 100644 plugins/modules/dcnm_contracts.py create mode 100644 plugins/modules/dcnm_protocols.py rename tests/{config.yaml => config.yml} (100%) create mode 100644 tests/integration/targets/dcnm_contracts/defaults/main.yaml create mode 100644 tests/integration/targets/dcnm_contracts/meta/main.yaml create mode 100644 tests/integration/targets/dcnm_contracts/tasks/dcnm.yaml create mode 100644 tests/integration/targets/dcnm_contracts/tasks/main.yaml create mode 100644 tests/integration/targets/dcnm_contracts/tests/dcnm/deleted.yaml create mode 100644 tests/integration/targets/dcnm_contracts/tests/dcnm/merged.yaml create mode 100644 tests/integration/targets/dcnm_contracts/tests/dcnm/overridden.yaml create mode 100644 tests/integration/targets/dcnm_contracts/tests/dcnm/query.yaml create mode 100644 tests/integration/targets/dcnm_contracts/tests/dcnm/replaced.yaml create mode 100644 tests/integration/targets/dcnm_protocols/defaults/main.yaml create mode 100644 tests/integration/targets/dcnm_protocols/meta/main.yaml create mode 100644 tests/integration/targets/dcnm_protocols/tasks/dcnm.yaml create mode 100644 tests/integration/targets/dcnm_protocols/tasks/main.yaml create mode 100644 tests/integration/targets/dcnm_protocols/tests/dcnm/deleted.yaml create mode 100644 tests/integration/targets/dcnm_protocols/tests/dcnm/merged.yaml create mode 100644 tests/integration/targets/dcnm_protocols/tests/dcnm/overridden.yaml create mode 100644 tests/integration/targets/dcnm_protocols/tests/dcnm/query.yaml create mode 100644 tests/integration/targets/dcnm_protocols/tests/dcnm/replaced.yaml create mode 100644 tests/unit/modules/dcnm/fixtures/dcnm_contracts/dcnm_contracts_common.py create mode 100644 tests/unit/modules/dcnm/fixtures/dcnm_contracts/dcnm_contracts_data.json create mode 100644 tests/unit/modules/dcnm/fixtures/dcnm_contracts/dcnm_contracts_response.json create mode 100644 tests/unit/modules/dcnm/fixtures/dcnm_protocols/dcnm_protocols_common.py create mode 100644 tests/unit/modules/dcnm/fixtures/dcnm_protocols/dcnm_protocols_data.json create mode 100644 tests/unit/modules/dcnm/fixtures/dcnm_protocols/dcnm_protocols_response.json create mode 100644 tests/unit/modules/dcnm/test_dcnm_contracts.py create mode 100644 tests/unit/modules/dcnm/test_dcnm_protocols.py diff --git a/README.md b/README.md index e631bd7e8..bb79c6eab 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ 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**. Plugins and modules within a collection may be tested with only specific Ansible versions. A collection may contain metadata that identifies these versions. @@ -32,6 +32,7 @@ Name | Description ### Modules Name | Description --- | --- +[cisco.dcnm.dcnm_contracts](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_contracts_module.rst)|Configure Contracts for security groups in NDFC fabrics [cisco.dcnm.dcnm_fabric](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_fabric_module.rst)|Manage creation and configuration of NDFC fabrics. [cisco.dcnm.dcnm_image_policy](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_image_policy_module.rst)|Image policy management for Nexus Dashboard Fabric Controller [cisco.dcnm.dcnm_image_upgrade](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_image_upgrade_module.rst)|Image management for Nexus switches @@ -41,6 +42,7 @@ Name | Description [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_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_protocols](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_protocols_module.rst)|Configure Protocols for security contracts on NDFC fabrics [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. [cisco.dcnm.dcnm_rest](https://github.com/CiscoDevNet/ansible-dcnm/blob/main/docs/cisco.dcnm.dcnm_rest_module.rst)|Send REST API requests to DCNM controller. [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. diff --git a/docs/cisco.dcnm.dcnm_contracts_module.rst b/docs/cisco.dcnm.dcnm_contracts_module.rst new file mode 100644 index 000000000..dde380c47 --- /dev/null +++ b/docs/cisco.dcnm.dcnm_contracts_module.rst @@ -0,0 +1,226 @@ +.. _cisco.dcnm.dcnm_contracts_module: + + +************************* +cisco.dcnm.dcnm_contracts +************************* + +**Configure Contracts for security groups in NDFC fabrics** + + +Version added: 3.5.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This module configures Contracts for security groups in NDFC fabrics + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ config + +
+ list + / elements=dictionary +
+
+ Default:
[]
+
+
List of dictionaries representing the contract configuration
+
Not required for 'query' and 'deleted' states
+
+
+ contract_name + +
+ string + / required +
+
+ +
Name of the contract
+
+
+ description + +
+ string +
+
+ +
Description of the contract
+
+
+ rules + +
+ list + / elements=dictionary + / required +
+
+ +
List of dictionaries representing the rules of the contract
+
+
+ action + +
+ string + / required +
+
+
    Choices: +
  • permit
  • +
  • permit_log
  • +
  • deny
  • +
  • deby_log
  • +
+
+
Action to be taken on the traffic
+
+
+ direction + +
+ string + / required +
+
+
    Choices: +
  • bidirectional
  • +
  • unidirectional
  • +
+
+
Direction of traffic flow
+
+
+ protocol_name + +
+ string + / required +
+
+ +
Name of the protocol
+
+
+ fabric + +
+ string + / required +
+
+ +
Name of the target fabric for contract operations
+
+
+ state + +
+ string +
+
+
    Choices: +
  • merged ←
  • +
  • deleted
  • +
  • replaced
  • +
  • overridden
  • +
  • query
  • +
+
+
The required state of the contract configuration after module completion
+
+
+ + + + + + + + +Status +------ + + +Authors +~~~~~~~ + +- Praveen Ramoorthy(@praveenramoorthy) diff --git a/docs/cisco.dcnm.dcnm_protocols_module.rst b/docs/cisco.dcnm.dcnm_protocols_module.rst new file mode 100644 index 000000000..99d494d7b --- /dev/null +++ b/docs/cisco.dcnm.dcnm_protocols_module.rst @@ -0,0 +1,476 @@ +.. _cisco.dcnm.dcnm_protocols_module: + + +************************* +cisco.dcnm.dcnm_protocols +************************* + +**Configure Protocols for security contracts on NDFC fabrics** + + +Version added: 3.5.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This module configures Protocols for security contracts on NDFC fabrics. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ config + +
+ list + / elements=dictionary +
+
+ Default:
[]
+
+
A list of dictionaries representing the protocols configuration.
+
Not required for 'query' and 'deleted' states.
+
+
+ description + +
+ string +
+
+ +
Description of the protocol.
+
+
+ match + +
+ list + / elements=dictionary +
+
+ +
A list of dictionaries representing the match criteria.
+
+
+ destination_port_range + +
+ string +
+
+ Default:
""
+
+
Destination port range.
+
+
+ dscp + +
+ integer +
+
+ +
DSCP value.
+
+
+ fragments + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Match fragments.
+
+
+ protocol_options + +
+ string +
+
+ Default:
""
+
+
Protocol options.
+
+
+ source_port_range + +
+ string +
+
+ Default:
""
+
+
Source port range.
+
+
+ stateful + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Match stateful connections.
+
+
+ tcp_flags + +
+ string +
+
+
    Choices: +
  • est
  • +
  • ack
  • +
  • fin
  • +
  • syn
  • +
  • rst
  • +
  • psh
  • +
+ Default:
""
+
+
TCP flags.
+
+
+ type + +
+ string + / required +
+
+
    Choices: +
  • ip
  • +
  • ipv4
  • +
  • ipv6
  • +
+
+
Type of the protocol.
+
+
+ match_all + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
Match all traffic.
+
+
+ protocol_name + +
+ string + / required +
+
+ +
Name of the protocol.
+
+
+ fabric + +
+ string + / required +
+
+ +
Name of the target fabric for protocols operations.
+
+
+ state + +
+ string +
+
+
    Choices: +
  • merged ←
  • +
  • deleted
  • +
  • replaced
  • +
  • overridden
  • +
  • query
  • +
+
+
The required state of the protocols configuration after module completion.
+
+
+ + + + +Examples +-------- + +.. code-block:: yaml + + # This module supports the following states: + # + # Merged: + # Protocols defined in the playbook will be merged into the target fabric. + # - If the protocol does not exist it will be added. + # - If the protocol exists but properties managed by the playbook are different + # they will be updated if possible. + # - Protocols that are not specified in the playbook will be untouched. + # + # Replaced: + # Protocols defined in the playbook will be replaced in the target fabric. + # - If the protocol does not exist it will be added. + # - If the protocol exists but properties managed by the playbook are different + # they will be updated if possible. + # - Properties that can be managed by the module but are not specified + # in the playbook will be deleted or defaulted if possible. + # - Protocols that are not specified in the playbook will be untouched. + # + # Overridden: + # Protocols defined in the playbook will be overridden in the target fabric. + # - If the protocol does not exist it will be added. + # - If the protocol exists but properties managed by the playbook are different + # they will be updated if possible. + # - Properties that can be managed by the module but are not specified + # in the playbook will be deleted or defaulted if possible. + # - Protocols that are not specified in the playbook will be deleted. + # + # Deleted: + # Protocols defined in the playbook will be deleted. + # If no protocol are provided in the playbook, all protocols present on that DCNM fabric will be deleted. + # + # Query: + # Returns the current DCNM state for the protocols listed in the playbook. + # If no protocols are provided in the playbook, all protocols present on that DCNM fabric will be returned. + + # Merged state - Add a new protocol + # The following example adds a new protocol to the fabric. + # If the protocol already exists, the module will update the protocol with the new configuration. + + - name: Add a new protocol + cisco.dcnm.dcnm_protocols: + fabric: vxlan-fabric + state: merged + config: + - protocol_name: protocol1 + description: "Protocol 1" + match_all: false + match: + - type: ip + protocol_options: tcp + fragments: false + stateful: false + source_port_range: "20-30" + destination_port_range: "50" + tcp_flags: "" + dscp: 16 + + # Replaced state - Replace an existing protocol + # The following example replaces an existing protocol protocol1 in the fabric. + # If the protocol does not exist, the module will create the protocol. + + - name: Replace an existing protocol + cisco.dcnm.dcnm_protocols: + fabric: vxlan-fabric + state: replaced + config: + - protocol_name: protocol1 + description: "Protocol 1" + match_all: false + match: + - type: ip + protocol_options: tcp + fragments: false + stateful: false + source_port_range: "10-40" + + # Overridden state - Override an existing protocol + # The following example overrides all existing protocol configuration in the fabric. + # If the protocol does not exist, the module will create the protocol. + # If the protocol exists, update the protocol with the new configuration. + # If the protocol exists but is not specified in the playbook, the module will delete the protocol. + + - name: Override all existing protocols + cisco.dcnm.dcnm_protocols: + fabric: vxlan-fabric + state: overridden + config: + - protocol_name: protocol1 + description: "Protocol 1" + match_all: false + match: + - type: ip + protocol_options: udp + source_port_range: "10-40" + + # Deleted state - Delete a protocol + # The following example deletes a protocol from the fabric. + + - name: Delete a protocol + cisco.dcnm.dcnm_protocols: + fabric: vxlan-fabric + state: deleted + config: + - protocol_name + + # If no protocol are provided in the playbook, all protocols present on that DCNM fabric will be deleted. + + - name: Delete all protocols + cisco.dcnm.dcnm_protocols: + fabric: vxlan-fabric + state: deleted + + # Query state - Query a protocol + # The following example queries a protocol from the fabric. + + - name: Query a protocol + cisco.dcnm.dcnm_protocols: + fabric: vxlan-fabric + state: query + config: + - protocol_name: protocol + + # If no protocol are provided in the playbook, all protocols present on that DCNM fabric will be returned. + + - name: Query all protocols + cisco.dcnm.dcnm_protocols: + fabric: vxlan-fabric + state: query + + + + +Status +------ + + +Authors +~~~~~~~ + +- Praveen Ramoorthy(@praveenramoorthy) diff --git a/plugins/module_utils/network/dcnm/dcnm_contracts_utils.py b/plugins/module_utils/network/dcnm/dcnm_contracts_utils.py new file mode 100644 index 000000000..a78764795 --- /dev/null +++ b/plugins/module_utils/network/dcnm/dcnm_contracts_utils.py @@ -0,0 +1,339 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import json + +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + dcnm_send, +) + +dcnm_contracts_get_paths = { + 11: {}, + 12: { + "NDFC_CONTRACT_GET": "/appcenter/cisco/ndfc/api/v1/security/fabrics/{}/contracts", + "NDFC_CONTRACT_CREATE": "/appcenter/cisco/ndfc/api/v1/security/fabrics/{}/contracts", + "NDFC_CONTRACT_DELETE": "/appcenter/cisco/ndfc/api/v1/security/fabrics/{}/contracts/bulkDelete", + "NDFC_CONTRACT_MODIFY": "/appcenter/cisco/ndfc/api/v1/security/fabrics/{}/contracts/{}" + }, +} + +# Some of the key names used in playbook is different from what is expected in payload. Translate such keys +xlate_key = { + "contract_name": "contractName", + "description": "description", + "rules": "rules", + "direction": "direction", + "action": "action", + "protocol_name": "protocolName", +} + + +def dcnm_contracts_utils_get_paths(dcnm_version): + + return dcnm_contracts_get_paths[dcnm_version] + + +def dcnm_contracts_utils_get_contracts_info(self, elem): + + if elem: + if elem.get("contractName", None): + contract_name = elem["contractName"] + else: + contract_name = elem["contract_name"] + path = self.paths["NDFC_CONTRACT_GET"] + '/' + contract_name + contracts = {} + else: + path = self.paths["NDFC_CONTRACT_GET"] + contracts = [] + + path = path.format(self.fabric) + resp = dcnm_send(self.module, "GET", path) + + if ( + resp + and (resp["RETURN_CODE"] == 200) + and (resp["MESSAGE"] == "OK") + and resp["DATA"] + ): + if elem: + contracts = [resp["DATA"]] + else: + contracts = resp["DATA"] + + for contract in contracts: + if "fabricName" in contract: + del contract["fabricName"] + if "associationCount" in contract: + del contract["associationCount"] + if contract.get("rules", None): + for rule in contract["rules"]: + if "uuid" in rule: + del rule["uuid"] + if "matchSummary" in rule: + del rule["matchSummary"] + + if elem: + return contracts[0] + else: + return contracts + + return contracts + + +def dcnm_contracts_utils_get_payload_elem(self, key, sel_info): + + pl_sel_info = [] + for elem in sel_info: + pl_elem = {} + for k in elem: + pl_elem[xlate_key[k]] = elem[k] + pl_sel_info.append(pl_elem) + return pl_sel_info + + +def dcnm_contracts_utils_get_contracts_payload(self, contracts_info): + + contracts_payload = {} + + for key in contracts_info: + if key == "rules" and contracts_info[key]: + sel_info = dcnm_contracts_utils_get_payload_elem(self, key, contracts_info[key]) + contracts_payload[xlate_key[key]] = sel_info + else: + contracts_payload[xlate_key[key]] = contracts_info[key] + + return contracts_payload + + +def dcnm_contracts_utils_compare_want_and_have(self, want): + + match_have = dcnm_contracts_utils_get_matching_have(self, want) + + for melem in match_have: + return dcnm_contracts_utils_compare_contracts_objects(self, want, melem) + + return "NDFC_CONTRACTS_CREATE", [] + + +def dcnm_contracts_utils_get_matching_want(self, contracts_info): + + match_want = [ + want + for want in self.want + if (contracts_info["contractName"] == want["contractName"]) + ] + + return match_want + + +def dcnm_contracts_utils_get_matching_have(self, want): + + match_have = [ + have + for have in self.have + if (have["contractName"] == want["contractName"]) + ] + + return match_have + + +def dcnm_contracts_utils_compare_contracts_objects(self, wobj, hobj): + + mismatch = False + + for key in wobj: + if str(hobj.get(key, None)) != str(wobj.get(key, None)): + if key == "rules": + if ( + self.module.params["state"] == "replaced" or + self.module.params["state"] == "overridden" + ): + if len(hobj["rules"]) != len(wobj["rules"]): + mismatch = True + if not mismatch: + for rule in wobj["rules"]: + if rule not in hobj["rules"]: + mismatch = True + break + else: + mismatch = True + break + + if mismatch: + return "NDFC_CONTRACTS_MERGE", hobj + else: + return "NDFC_CONTRACTS_EXIST", [] + + +def dcnm_contracts_utils_get_delete_payload(self, elem): + + return elem["contractName"] + + +def dcnm_contracts_utils_get_delete_list(self): + + del_list = [] + + # Get all security contract information present + contracts_info = dcnm_contracts_utils_get_contracts_info(self, None) + + if contracts_info == []: + 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 contract in contracts_info: + want = dcnm_contracts_utils_get_matching_want(self, contract) + if want == []: + if contract not in del_list: + del_list.append(contract) + + return del_list + + +def dcnm_contracts_utils_get_all_filtered_contracts_objects(self): + + contracts_list = dcnm_contracts_utils_get_contracts_info(self, None) + + # If filters are provided, use the values to build the appropriate list. + if self.contracts_info == []: + return contracts_list + else: + contracts_filtered_list = [] + filter_keys = set().union(*(d.values() for d in self.contracts_info)) + + for elem in contracts_list: + + match = False + + if (elem.get("contractName", 0) != 0) and ( + elem["contractName"] in filter_keys + ): + match = True + + if not match: + continue + + if elem not in contracts_filtered_list: + contracts_filtered_list.append(elem) + + return contracts_filtered_list + + +def dcnm_contracts_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["NDFC_CONTRACT_DELETE"] + path = path.format(self.fabric) + + json_payload = json.dumps(self.diff_delete) + + resp = dcnm_send(self.module, "POST", path, json_payload) + + 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 + + return delete_flag + + +def dcnm_contracts_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": + action = "CREATE" + elif command == "PUT": + action = "MODIFY" + + json_payload = None + if payload_list == []: + return flag + + if action == "CREATE": + json_payload = json.dumps(payload_list) + resp = dcnm_send(self.module, command, path, json_payload) + else: + for elem in payload_list: + json_payload = json.dumps(elem) + mod_path = path.format(self.fabric, elem["contractName"]) + + resp = dcnm_send(self.module, command, mod_path, json_payload) + + 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_contracts_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 + """ + + create_path = self.paths["NDFC_CONTRACT_CREATE"].format(self.fabric) + + return dcnm_contracts_utils_process_payloads_list(self, self.diff_create, "POST", create_path) + + +def dcnm_contracts_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 + """ + + modify_path = self.paths["NDFC_CONTRACT_MODIFY"] + + return dcnm_contracts_utils_process_payloads_list(self, self.diff_modify, "PUT", modify_path) diff --git a/plugins/module_utils/network/dcnm/dcnm_protocols_utils.py b/plugins/module_utils/network/dcnm/dcnm_protocols_utils.py new file mode 100644 index 000000000..fb07ca92e --- /dev/null +++ b/plugins/module_utils/network/dcnm/dcnm_protocols_utils.py @@ -0,0 +1,369 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import json + +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + dcnm_send, +) + + +dcnm_protocols_get_paths = { + 11: {}, + 12: { + "NDFC_PROTOCOL_GET": "/appcenter/cisco/ndfc/api/v1/security/fabrics/{}/protocols", + "NDFC_PROTOCOL_CREATE": "/appcenter/cisco/ndfc/api/v1/security/fabrics/{}/protocols", + "NDFC_PROTOCOL_DELETE": "/appcenter/cisco/ndfc/api/v1/security/fabrics/{}/protocols/bulkDelete", + "NDFC_PROTOCOL_MODIFY": "/appcenter/cisco/ndfc/api/v1/security/fabrics/{}/protocols/{}" + }, +} + +# Some of the key names used in playbook is different from what is expected in payload. Translate such keys +xlate_key = { + "protocol_name": "protocolName", + "description": "description", + "match_all": "matchType", + "match": "matchItems", + "type": "type", + "protocol_options": "protocolOptions", + "fragments": "onlyFragments", + "stateful": "stateful", + "source_port_range": "srcPortRange", + "destination_port_range": "dstPortRange", + "tcp_flags": "tcpFlags", + "dscp": "dscp", +} + + +def dcnm_protocols_utils_get_paths(dcnm_version): + + return dcnm_protocols_get_paths[dcnm_version] + + +def dcnm_protocols_utils_get_protocols_info(self, elem): + + if elem: + if elem.get("protocolName", None): + proto_name = elem["protocolName"] + else: + proto_name = elem["protocol_name"] + path = self.paths["NDFC_PROTOCOL_GET"] + '/' + proto_name + protocols = {} + else: + path = self.paths["NDFC_PROTOCOL_GET"] + protocols = [] + + path = path.format(self.fabric) + resp = dcnm_send(self.module, "GET", path) + + if ( + resp + and (resp["RETURN_CODE"] == 200) + and (resp["MESSAGE"] == "OK") + and resp["DATA"] + ): + if elem: + protocols = [resp["DATA"]] + else: + protocols = resp["DATA"] + + for protocol in protocols: + if "fabricName" in protocol: + del protocol["fabricName"] + if "associatedContractCount" in protocol: + del protocol["associatedContractCount"] + if "matchItems" in protocol: + for match in protocol["matchItems"]: + if "matchSummary" in match: + del match["matchSummary"] + if match.get("protocolOptions", None): + match["protocolOptions"] = match["protocolOptions"].lower() + if match.get("type", None): + match["type"] = match["type"].lower() + + if elem: + return protocols[0] + else: + return protocols + + return protocols + + +def dcnm_protocols_utils_get_payload_elem(self, key, sel_info): + + pl_sel_info = [] + for elem in sel_info: + pl_elem = {} + for k in elem: + pl_elem[xlate_key[k]] = elem[k] + pl_sel_info.append(pl_elem) + return pl_sel_info + + +def dcnm_protocols_utils_get_protocols_payload(self, protocols_info): + + protocols_payload = {} + + for key in protocols_info: + if key == "match" and protocols_info[key]: + sel_info = dcnm_protocols_utils_get_payload_elem(self, key, protocols_info[key]) + protocols_payload[xlate_key[key]] = sel_info + else: + if key == "match_all": + protocols_payload[xlate_key[key]] = "any" + else: + protocols_payload[xlate_key[key]] = protocols_info[key] + return protocols_payload + + +def dcnm_protocols_utils_compare_want_and_have(self, want): + + match_have = dcnm_protocols_utils_get_matching_have(self, want) + + for melem in match_have: + return dcnm_protocols_utils_compare_protocols_objects(self, want, melem) + + return "NDFC_PROTOCOLS_CREATE", [] + + +def dcnm_protocols_utils_get_matching_want(self, protocols_info): + + match_want = [ + want + for want in self.want + if (protocols_info["protocolName"] == want["protocolName"]) + ] + + return match_want + + +def dcnm_protocols_utils_get_matching_have(self, want): + + match_have = [ + have + for have in self.have + if (have["protocolName"] == want["protocolName"]) + ] + + return match_have + + +def dcnm_protocols_utils_compare_protocols_objects(self, wobj, hobj): + + mismatch = False + + for key in wobj: + if str(hobj.get(key, None)) != str(wobj.get(key, None)): + if key == "matchItems": + if ( + self.module.params["state"] == "replaced" or + self.module.params["state"] == "overridden" + ): + if len(wobj["matchItems"]) != len(hobj["matchItems"]): + mismatch = True + if not mismatch: + for match in wobj["matchItems"]: + if match not in hobj["matchItems"]: + mismatch = True + break + else: + mismatch = True + + if mismatch: + return "NDFC_PROTOCOLS_MERGE", hobj + else: + return "NDFC_PROTOCOLS_EXIST", [] + + +def dcnm_protocols_utils_get_delete_payload(self, elem): + + return elem["protocolName"] + + +def dcnm_protocols_utils_get_delete_list(self): + + del_list = [] + + # Get all security protocol information present + protocols_info = dcnm_protocols_utils_get_protocols_info(self, None) + + if protocols_info == []: + 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 protocol in protocols_info: + want = dcnm_protocols_utils_get_matching_want(self, protocol) + if want == []: + if protocol not in del_list: + del_list.append(protocol) + + return del_list + + +def dcnm_protocols_utils_get_all_filtered_protocols_objects(self): + + protocols_list = dcnm_protocols_utils_get_protocols_info(self, None) + + # If filters are provided, use the values to build the appropriate list. + if self.protocols_info == []: + playbook_payload = [] + for elem in protocols_list: + if "fabricName" in elem: + del elem["fabricName"] + if "associatedContractCount" in elem: + del elem["associatedContractCount"] + if elem.get("matchItems", None): + for match in elem["matchItems"]: + if "matchSummary" in match: + del match["matchSummary"] + playbook_payload.append(elem) + + return playbook_payload + else: + protocols_filtered_list = [] + filter_keys = set().union(*(d.values() for d in self.protocols_info)) + + for elem in protocols_list: + + match = False + + if (elem.get("protocolName", 0) != 0) and ( + elem["protocolName"] in filter_keys + ): + match = True + + if not match: + continue + + if "fabricName" in elem: + del elem["fabricName"] + if "associatedContractCount" in elem: + del elem["associatedContractCount"] + if elem.get("matchItems", None): + for match in elem["matchItems"]: + if "matchSummary" in match: + del match["matchSummary"] + + if elem not in protocols_filtered_list: + protocols_filtered_list.append(elem) + + return protocols_filtered_list + + +def dcnm_protocols_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["NDFC_PROTOCOL_DELETE"] + path = path.format(self.fabric) + + json_payload = json.dumps(self.diff_delete) + + resp = dcnm_send(self.module, "POST", path, json_payload) + + 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 + + return delete_flag + + +def dcnm_protocols_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": + action = "CREATE" + elif command == "PUT": + action = "MODIFY" + + json_payload = None + if payload_list == []: + return flag + + if action == "CREATE": + json_payload = json.dumps(payload_list) + resp = dcnm_send(self.module, command, path, json_payload) + else: + for elem in payload_list: + json_payload = json.dumps(elem) + mod_path = path.format(self.fabric, elem["protocolName"]) + resp = dcnm_send(self.module, command, mod_path, json_payload) + + 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_protocols_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 + """ + + create_path = self.paths["NDFC_PROTOCOL_CREATE"].format(self.fabric) + + return dcnm_protocols_utils_process_payloads_list(self, self.diff_create, "POST", create_path) + + +def dcnm_protocols_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 + """ + + modify_path = self.paths["NDFC_PROTOCOL_MODIFY"] + + return dcnm_protocols_utils_process_payloads_list(self, self.diff_modify, "PUT", modify_path) diff --git a/plugins/modules/dcnm_contracts.py b/plugins/modules/dcnm_contracts.py new file mode 100644 index 000000000..b960770dd --- /dev/null +++ b/plugins/modules/dcnm_contracts.py @@ -0,0 +1,751 @@ +#!/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__ = "Praveen Ramoorthy" + +DOCUMENTATION = """ +module: dcnm_contracts +short_description: Configure Contracts for security groups in NDFC fabrics +version_added: 3.5.0 +description: + - "This module configures Contracts for security groups in NDFC fabrics" +author: Praveen Ramoorthy(@praveenramoorthy) +options: + fabric: + description: + - Name of the target fabric for contract operations + type: str + required: true + state: + description: + - The required state of the contract configuration after module completion + type: str + required: false + choices: ['merged', 'deleted', 'replaced', 'overridden', 'query'] + default: 'merged' + config: + description: + - List of dictionaries representing the contract configuration + - Not required for 'query' and 'deleted' states + type: list + elements: dict + default: [] + suboptions: + contract_name: + description: + - Name of the contract + type: str + required: true + description: + description: + - Description of the contract + type: str + required: false + rules: + description: + - List of dictionaries representing the rules of the contract + type: list + required: true + elements: dict + suboptions: + direction: + description: + - Direction of traffic flow + type: str + required: true + choices: ['bidirectional', 'unidirectional'] + action: + description: + - Action to be taken on the traffic + type: str + required: true + choices: ['permit', 'permit_log', 'deny', 'deby_log'] + protocol_name: + description: + - Name of the protocol + type: str + required: true +""" + +EXAMPLES = """ +# This module supports the following states: +# +# Merged: +# Contracts defined in the playbook will be merged into the target fabric. +# - If the contract does not exist it will be added. +# - If the contract exists but properties managed by the playbook are different +# they will be updated if possible. +# - Contracts that are not specified in the playbook will be untouched. +# +# Replaced: +# Contracts defined in the playbook will be replaced in the target fabric. +# - If the contract does not exist it will be added. +# - If the contract exists but properties managed by the playbook are different +# they will be updated if possible. +# - Properties that can be managed by the module but are not specified +# in the playbook will be deleted or defaulted if possible. +# - Contracts that are not specified in the playbook will be untouched. +# +# Overridden: +# Contracts defined in the playbook will be overridden in the target fabric. +# - If the contract does not exist it will be added. +# - If the contract exists but properties managed by the playbook are different +# they will be updated if possible. +# - Properties that can be managed by the module but are not specified +# in the playbook will be deleted or defaulted if possible. +# - Contracts that are not specified in the playbook will be deleted. +# +# Deleted: +# Contracts defined in the playbook will be deleted. +# If no contracts are provided in the playbook, all contracts present on that DCNM fabric will be deleted. +# +# Query: +# Returns the current DCNM state for the contracts listed in the playbook. +# If no contracts are provided in the playbook, all contracts present on that DCNM fabric will be returned. + + +# Merged state - Add or update contracts. +# The below example adds a contract named 'contract1' with a description and rules. + +- name: Add a contract + cisco.dcnm.dcnm_contracts: + fabric: 'fab1' + state: 'merged' + config: + - contract_name: 'contract1' + description: 'Contract 1' + rules: + - direction: 'bidirectional' + action: 'permit' + protocol_name: 'tcp' + - direction: 'unidirectional' + action: 'deny' + protocol_name: 'udp' + +# Replaced state - Replace contracts. +# The below example replaces the existing contract named 'contract1' with a new description and rules. +# If the contract does not exist, it will be created. + +- name: Replace a contract + cisco.dcnm.dcnm_contracts: + fabric: 'fab1' + state: 'replaced' + config: + - contract_name: 'contract1' + description: 'Contract 1 updated' + rules: + - direction: 'bidirectional' + action: 'permit_log' + protocol_name: 'https' + - direction: 'unidirectional' + action: 'deny_log' + protocol_name: 'http' + +# Overridden state - Override contracts. +# The below example overrides all contracts in the fabric with the contracts defined in the playbook. +# If a contract in playbook does not exist, it will be created. If a contract exists, it will be updated. +# If a contract exists in the fabric but not in the playbook, it will be deleted. + +- name: Override contracts + cisco.dcnm.dcnm_contracts: + fabric: 'fab1' + state: 'overridden' + config: + - contract_name: 'contract1' + description: 'Contract 1 updated' + rules: + - direction: 'bidirectional' + action: 'permit_log' + protocol_name: 'https' + - direction: 'unidirectional' + action: 'deny_log' + protocol_name: 'http' + - contract_name: 'contract2' + description: 'Contract 2' + rules: + - direction: 'bidirectional' + action: 'permit' + protocol_name: 'tcp' + +# Deleted state - Delete contracts. +# The below example deletes the contracts named 'contract1' and 'contract2'. + +- name: Delete contracts + cisco.dcnm.dcnm_contracts: + fabric: 'fab1' + state: 'deleted' + config: + - contract_name: 'contract1' + - contract_name: 'contract2' + +# If no contracts are provided in the playbook, all contracts present on that DCNM fabric will be deleted. + +- name: Delete all contracts + cisco.dcnm.dcnm_contracts: + fabric: 'fab1' + state: 'deleted' + +# Query state - Query contracts. +# The below example queries the contracts named 'contract1' and 'contract2'. + +- name: Query contracts + cisco.dcnm.dcnm_contracts: + fabric: 'fab1' + state: 'query' + config: + - contract_name: 'contract1' + - contract_name: 'contract2' + +# If no contracts are provided in the playbook, all contracts present on that DCNM fabric will be returned. + +- name: Query all contracts + cisco.dcnm.dcnm_contracts: + fabric: 'fab1' + state: 'query' +""" +import copy + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + validate_list_of_dicts, + dcnm_version_supported, + get_fabric_inventory_details, + get_fabric_details, +) + +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm_contracts_utils import ( + dcnm_contracts_utils_get_paths, + dcnm_contracts_utils_get_contracts_info, + dcnm_contracts_utils_get_contracts_payload, + dcnm_contracts_utils_compare_want_and_have, + dcnm_contracts_utils_process_delete_payloads, + dcnm_contracts_utils_process_create_payloads, + dcnm_contracts_utils_process_modify_payloads, + dcnm_contracts_utils_get_delete_payload, + dcnm_contracts_utils_get_delete_list, + dcnm_contracts_utils_get_all_filtered_contracts_objects, +) + + +# +# 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 +# + + +# Resource Class object which includes all the required methods and data to configure and maintain Contracts +class DcnmContracts: + + def __init__(self, module): + self.module = module + self.params = module.params + self.fabric = module.params["fabric"] + self.config = copy.deepcopy(module.params.get("config", [])) + self.contracts_info = [] + self.want = [] + self.have = [] + self.diff_create = [] + self.diff_modify = [] + self.diff_delete = [] + self.arg_specs = {} + self.fd = None + self.changed_dict = [ + { + "merged": [], + "deleted": [], + "modified": [], + "query": [], + "debugs": [], + } + ] + + self.result = dict(changed=False, diff=[], response=[]) + + def log_msg(self, msg): + + if self.fd is None: + self.fd = open("dcnm_contracts.log", "a+") + if self.fd is not None: + self.fd.write(msg) + self.fd.write("\n") + self.fd.flush() + + def dcnm_contracts_merge_want_and_have(self, want, have): + + """ + Routine to check for mergeable keys in want and merge the same with whatever is already exsiting + in have. + + Parameters: + want (dict): Object to be updated with information from have + have (dict): Existing CONTRACTS information + + Returns: + None + """ + + # Code is generated for comparing "nvPairs" objects alone. If there are other nested structures + # in the want and have objects that need to be compared, add the necessary code here. + + # There may be certain objects like "Freeform config" in the parameters which + # inlcude a list of commands or parameters like member ports which inlcudes a + # list of interfaces. During MERGE, the values from WANT should be merged + # to values in have. Identify the actual keys in WANT and HAVE and update the + # below block of CODE to achieve the merge + + if want.get("description", None): + have["description"] = want["description"] + + if want.get("rules", None): + if have.get("rules", None): + for rule in have["rules"]: + if rule not in want["rules"]: + want["rules"].append(rule) + + def dcnm_contracts_get_diff_query(self): + + """ + Routine to retrieve CONTRACTS from controller. This routine extracts information provided by the + user and filters the output based on that. + + Parameters: + None + + Returns: + None + """ + + contracts_list = dcnm_contracts_utils_get_all_filtered_contracts_objects(self) + if contracts_list != []: + self.result["response"].extend(contracts_list) + + def dcnm_contracts_get_diff_overridden(self, cfg): + + """ + Routine to override existing CONTRACTS information with what is included in the playbook. This routine + deletes all CONTRACTS 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_contracts_utils_get_delete_list(self) + + # 'del_list' contains all CONTRACTS information in 'have' format. Use that to update delete payloads + + for elem in del_list: + self.dcnm_contracts_update_delete_payloads(elem) + + if cfg == []: + return + + if self.want: + # New configuration is included. Delete all existing CONTRACTS objects and create new objects as requested + # through the configuration + self.dcnm_contracts_get_diff_merge() + + def dcnm_contracts_get_diff_deleted(self): + + """ + Routine to get a list of payload information that will be used to delete Contracts. + This routine updates self.diff_delete with payloads that are used to delete Contracts + from the server. + + Parameters: + None + + Returns: + None + """ + + if self.contracts_info == []: + # User has not included any config. Delete all existing CONTRACTS objects from DCNM + self.dcnm_contracts_get_diff_overridden([]) + return + + for elem in self.contracts_info: + + # Perform any translations that may be required on the contracts_info. + have = dcnm_contracts_utils_get_contracts_info(self, elem) + + if have != {}: + self.dcnm_contracts_update_delete_payloads(have) + + def dcnm_contracts_update_delete_payloads(self, have): + + # Get the delete payload based on 'have' + del_payload = dcnm_contracts_utils_get_delete_payload(self, have) + + if del_payload != {} and del_payload not in self.diff_delete: + self.changed_dict[0]["deleted"].append( + del_payload + ) + self.diff_delete.append(del_payload) + + def dcnm_contracts_get_diff_merge(self): + + """ + Routine to populate a list of payload information in self.diff_create to create/update Contracts. + + Parameters: + None + + Returns: + None + """ + + if not self.want: + return + + for elem in self.want: + + rc, have = dcnm_contracts_utils_compare_want_and_have(self, elem) + + if rc == "NDFC_CONTRACTS_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 == "NDFC_CONTRACTS_MERGE": + # Object already exists, and needs an update + if elem not in self.diff_modify: + self.changed_dict[0]["modified"].append(elem) + + # Fields like CONF which are a list of commands should be handled differently in this case. + # For existing objects, we will have to merge the current list of commands with already existing + # ones in have. For replace, no need to merge them. They must be replaced with what is given. + if self.module.params["state"] == "merged": + self.dcnm_contracts_merge_want_and_have(elem, have) + self.diff_modify.append(elem) + + def dcnm_contracts_get_want(self): + + """ + This routine updates self.want with the payload information based on the playbook configuration. + + Parameters: + None + + Returns: + None + """ + + if [] is self.config: + return + + if not self.contracts_info: + return + + for elem in self.contracts_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_contracts_get_payload(elem) + if payload not in self.want: + self.want.append(payload) + + def dcnm_contracts_get_have(self): + + """ + Routine to get exisitng contracts information from DCNM that matches information in self.want. + This routine updates self.have with all the contracts that match the given playbook configuration + + Parameters: + None + + Returns: + None + """ + + if self.want == []: + return + + have = dcnm_contracts_utils_get_contracts_info(self, None) + if (have != []): + self.have = have + + def dcnm_contracts_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.contracts_info with + validated playbook information related to deleted state. + + Parameters: + cfg (dict): The config from playbook + + Returns: + None + """ + + arg_spec = {'contract_name': {'type': 'str'}} + + contracts_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 contracts_info: + self.contracts_info.extend(contracts_info) + + def dcnm_contracts_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.contracts_info with + validated playbook information related to query state. + + Parameters: + cfg (dict): The config from playbook + + Returns: + None + """ + + arg_spec = {'contract_name': {'type': 'str'}} + + contracts_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 contracts_info: + self.contracts_info.extend(contracts_info) + + def dcnm_contracts_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 = { + 'contract_name': {'required': True, 'type': 'str'}, + 'description': {'type': 'str'}, + 'rules': {'required': True, 'type': 'list', 'elements': 'dict'} + } + rules_spec = { + 'direction': {'required': True, 'type': 'str', 'choices': ['bidirectional', 'unidirectional']}, + 'action': {'required': True, 'type': 'str', 'choices': ['permit', 'permit_log', 'deny', 'deby_log']}, + 'protocol_name': {'required': True, 'type': 'str'} + } + + # Even 'common_spec' may require some updates based on other information. + # dcnm_contracts_utils_update_common_spec(self, common_spec) + + contracts_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) + + for contract in contracts_info: + if contract.get("rules"): + rules_info, invalid_att = validate_list_of_dicts(contract['rules'], rules_spec) + contract["rules"] = rules_info + invalid_params.extend(invalid_att) + + self.contracts_info.append(contract) + + if invalid_params: + mesg = "Invalid parameters in playbook: {0}".format(invalid_params) + self.module.fail_json(msg=mesg) + + def dcnm_contracts_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 [] is 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_contracts_validate_query_state_input(cfg) + elif self.module.params["state"] == "deleted": + # config for deleted state is different. So validate deleted state differently + self.dcnm_contracts_validate_deleted_state_input(cfg) + else: + self.dcnm_contracts_validate_input(cfg) + cfg.remove(citem) + + def dcnm_contracts_get_payload(self, contracts_info): + + """ + This routine builds the complete object payload based on the information in self.want + + Parameters: + contracts_info (dict): Object information + + Returns: + contracts_payload (dict): Object payload information populated with appropriate data from playbook config + """ + + contracts_payload = dcnm_contracts_utils_get_contracts_payload(self, contracts_info) + + return contracts_payload + + def dcnm_contracts_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 + + delete_flag = dcnm_contracts_utils_process_delete_payloads(self) + create_flag = dcnm_contracts_utils_process_create_payloads(self) + modify_flag = dcnm_contracts_utils_process_modify_payloads(self) + + self.result["changed"] = ( + create_flag or modify_flag or delete_flag + ) + + def dcnm_contracts_update_module_info(self): + + """ + Routine to update version and fabric details + + Parameters: + None + + Returns: + None + """ + + self.dcnm_version = dcnm_version_supported(self.module) + self.inventory_data = get_fabric_inventory_details( + self.module, self.fabric + ) + + self.fabric_info = get_fabric_details(self.module, self.fabric) + self.paths = dcnm_contracts_utils_get_paths(self.dcnm_version) + + +def main(): + + """ main entry point for module execution + """ + element_spec = dict( + fabric=dict(required=True, type="str"), + config=dict(required=False, type="list", elements="dict", default=[]), + state=dict( + type="str", + default="merged", + choices=["merged", "deleted", "replaced", "overridden", "query"], + ), + ) + + module = AnsibleModule( + argument_spec=element_spec, supports_check_mode=True + ) + + dcnm_contracts = DcnmContracts(module) + + # Fill up the version and fabric related details + dcnm_contracts.dcnm_contracts_update_module_info() + + state = module.params["state"] + + if [] is dcnm_contracts.config: + if state == "merged" or state == "replaced": + module.fail_json( + msg="'config' element is mandatory for state '{0}', given = '{1}'".format( + state, dcnm_contracts.config + ) + ) + + dcnm_contracts.dcnm_contracts_validate_all_input() + + if ( + module.params["state"] != "query" and + module.params["state"] != "deleted" + ): + dcnm_contracts.dcnm_contracts_get_want() + + dcnm_contracts.dcnm_contracts_get_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. + + if (module.params["state"] == "merged") or ( + module.params["state"] == "replaced" + ): + dcnm_contracts.dcnm_contracts_get_diff_merge() + + if module.params["state"] == "deleted": + dcnm_contracts.dcnm_contracts_get_diff_deleted() + + if module.params["state"] == "overridden": + dcnm_contracts.dcnm_contracts_get_diff_overridden(dcnm_contracts.config) + + if module.params["state"] == "query": + dcnm_contracts.dcnm_contracts_get_diff_query() + + dcnm_contracts.result["diff"] = dcnm_contracts.changed_dict + + if dcnm_contracts.diff_create or dcnm_contracts.diff_delete or dcnm_contracts.diff_modify: + dcnm_contracts.result["changed"] = True + + if module.check_mode: + dcnm_contracts.result["changed"] = False + module.exit_json(**dcnm_contracts.result) + + dcnm_contracts.dcnm_contracts_send_message_to_dcnm() + + module.exit_json(**dcnm_contracts.result) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/dcnm_protocols.py b/plugins/modules/dcnm_protocols.py new file mode 100644 index 000000000..703c13db5 --- /dev/null +++ b/plugins/modules/dcnm_protocols.py @@ -0,0 +1,818 @@ +#!/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__ = "Praveen Ramoorthy" + +DOCUMENTATION = """ +--- +module: dcnm_protocols +short_description: Configure Protocols for security contracts on NDFC fabrics +version_added: 3.5.0 +description: + - "This module configures Protocols for security contracts on NDFC fabrics." +author: Praveen Ramoorthy(@praveenramoorthy) +options: + fabric: + description: + - Name of the target fabric for protocols operations. + type: str + required: yes + state: + description: + - The required state of the protocols configuration after module completion. + type: str + choices: ['merged', 'deleted', 'replaced', 'overridden', 'query'] + default: merged + config: + description: + - A list of dictionaries representing the protocols configuration. + - Not required for 'query' and 'deleted' states. + type: list + elements: dict + default: [] + suboptions: + protocol_name: + description: + - Name of the protocol. + type: str + required: yes + description: + description: + - Description of the protocol. + type: str + match_all: + description: + - Match all traffic. + type: bool + default: false + match: + description: + - A list of dictionaries representing the match criteria. + type: list + elements: dict + suboptions: + type: + description: + - Type of the protocol. + type: str + required: yes + choices: ['ip', 'ipv4', 'ipv6'] + protocol_options: + description: + - Protocol options. + type: str + default: "" + fragments: + description: + - Match fragments. + type: bool + default: false + stateful: + description: + - Match stateful connections. + type: bool + default: false + source_port_range: + description: + - Source port range. + type: str + default: "" + destination_port_range: + description: + - Destination port range. + type: str + default: "" + tcp_flags: + description: + - TCP flags. + type: str + choices: ['est', 'ack', 'fin', 'syn', 'rst', 'psh'] + default: "" + dscp: + description: + - DSCP value. + type: int +""" + +EXAMPLES = """ +# This module supports the following states: +# +# Merged: +# Protocols defined in the playbook will be merged into the target fabric. +# - If the protocol does not exist it will be added. +# - If the protocol exists but properties managed by the playbook are different +# they will be updated if possible. +# - Protocols that are not specified in the playbook will be untouched. +# +# Replaced: +# Protocols defined in the playbook will be replaced in the target fabric. +# - If the protocol does not exist it will be added. +# - If the protocol exists but properties managed by the playbook are different +# they will be updated if possible. +# - Properties that can be managed by the module but are not specified +# in the playbook will be deleted or defaulted if possible. +# - Protocols that are not specified in the playbook will be untouched. +# +# Overridden: +# Protocols defined in the playbook will be overridden in the target fabric. +# - If the protocol does not exist it will be added. +# - If the protocol exists but properties managed by the playbook are different +# they will be updated if possible. +# - Properties that can be managed by the module but are not specified +# in the playbook will be deleted or defaulted if possible. +# - Protocols that are not specified in the playbook will be deleted. +# +# Deleted: +# Protocols defined in the playbook will be deleted. +# If no protocol are provided in the playbook, all protocols present on that DCNM fabric will be deleted. +# +# Query: +# Returns the current DCNM state for the protocols listed in the playbook. +# If no protocols are provided in the playbook, all protocols present on that DCNM fabric will be returned. + +# Merged state - Add a new protocol +# The following example adds a new protocol to the fabric. +# If the protocol already exists, the module will update the protocol with the new configuration. + +- name: Add a new protocol + cisco.dcnm.dcnm_protocols: + fabric: vxlan-fabric + state: merged + config: + - protocol_name: protocol1 + description: "Protocol 1" + match_all: false + match: + - type: ip + protocol_options: tcp + fragments: false + stateful: false + source_port_range: "20-30" + destination_port_range: "50" + tcp_flags: "" + dscp: 16 + +# Replaced state - Replace an existing protocol +# The following example replaces an existing protocol protocol1 in the fabric. +# If the protocol does not exist, the module will create the protocol. + +- name: Replace an existing protocol + cisco.dcnm.dcnm_protocols: + fabric: vxlan-fabric + state: replaced + config: + - protocol_name: protocol1 + description: "Protocol 1" + match_all: false + match: + - type: ip + protocol_options: tcp + fragments: false + stateful: false + source_port_range: "10-40" + +# Overridden state - Override an existing protocol +# The following example overrides all existing protocol configuration in the fabric. +# If the protocol does not exist, the module will create the protocol. +# If the protocol exists, update the protocol with the new configuration. +# If the protocol exists but is not specified in the playbook, the module will delete the protocol. + +- name: Override all existing protocols + cisco.dcnm.dcnm_protocols: + fabric: vxlan-fabric + state: overridden + config: + - protocol_name: protocol1 + description: "Protocol 1" + match_all: false + match: + - type: ip + protocol_options: udp + source_port_range: "10-40" + +# Deleted state - Delete a protocol +# The following example deletes a protocol from the fabric. + +- name: Delete a protocol + cisco.dcnm.dcnm_protocols: + fabric: vxlan-fabric + state: deleted + config: + - protocol_name + +# If no protocol are provided in the playbook, all protocols present on that DCNM fabric will be deleted. + +- name: Delete all protocols + cisco.dcnm.dcnm_protocols: + fabric: vxlan-fabric + state: deleted + +# Query state - Query a protocol +# The following example queries a protocol from the fabric. + +- name: Query a protocol + cisco.dcnm.dcnm_protocols: + fabric: vxlan-fabric + state: query + config: + - protocol_name: protocol + +# If no protocol are provided in the playbook, all protocols present on that DCNM fabric will be returned. + +- name: Query all protocols + cisco.dcnm.dcnm_protocols: + fabric: vxlan-fabric + state: query +""" + +import copy + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm import ( + validate_list_of_dicts, + dcnm_version_supported, + get_fabric_inventory_details, + get_fabric_details, +) + +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm_protocols_utils import ( + dcnm_protocols_utils_get_paths, + dcnm_protocols_utils_get_protocols_info, + dcnm_protocols_utils_get_protocols_payload, + dcnm_protocols_utils_compare_want_and_have, + dcnm_protocols_utils_process_delete_payloads, + dcnm_protocols_utils_process_create_payloads, + dcnm_protocols_utils_process_modify_payloads, + dcnm_protocols_utils_get_delete_payload, + dcnm_protocols_utils_get_delete_list, + dcnm_protocols_utils_get_all_filtered_protocols_objects, +) + +# +# 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 +# + +# Resource Class object which includes all the required methods and data to configure and maintain Protocols + + +class DcnmProtocols: + def __init__(self, module): + self.module = module + self.params = module.params + self.fabric = module.params["fabric"] + self.config = copy.deepcopy(module.params.get("config", [])) + self.protocols_info = [] + self.want = [] + self.have = [] + self.diff_create = [] + self.diff_modify = [] + self.diff_delete = [] + self.arg_specs = {} + self.fd = None + self.changed_dict = [ + { + "merged": [], + "deleted": [], + "modified": [], + "query": [], + "debugs": [], + } + ] + + self.result = dict(changed=False, diff=[], response=[]) + + def log_msg(self, msg): + + if self.fd is None: + self.fd = open("dcnm_protocols.log", "a+") + if self.fd is not None: + self.fd.write(msg) + self.fd.write("\n") + self.fd.flush() + + def dcnm_protocols_merge_want_and_have(self, want, have): + + """ + Routine to check for mergeable keys in want and merge the same with whatever is already exsiting + in have. + + Parameters: + want (dict): Object to be updated with information from have + have (dict): Existing PROTOCOLS information + + Returns: + None + """ + + defaulted_keys = [] + + # There may be certain objects like "Freeform config" in the parameters which + # inlcude a list of commands or parameters like member ports which inlcudes a + # list of interfaces. During MERGE, the values from WANT should be merged + # to values in have. Identify the actual keys in WANT and HAVE and update the + # below block of CODE to achieve the merge + + if want.get("description", None): + have["description"] = want["description"] + + if want.get("matchItems", None): + if have.get("matchItems", None): + for match in have["matchItems"]: + if match not in want["matchItems"]: + want["matchItems"].append(match) + + def dcnm_protocols_get_diff_query(self): + + """ + Routine to retrieve PROTOCOLS from controller. This routine extracts information provided by the + user and filters the output based on that. + + Parameters: + None + + Returns: + None + """ + + protocols_list = dcnm_protocols_utils_get_all_filtered_protocols_objects(self) + if protocols_list != []: + self.result["response"].extend(protocols_list) + + def dcnm_protocols_get_diff_overridden(self, cfg): + + """ + Routine to override existing PROTOCOLS information with what is included in the playbook. This routine + deletes all PROTOCOLS 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_protocols_utils_get_delete_list(self) + + # 'del_list' contains all PROTOCOLS information in 'have' format. Use that to update delete and delte + + for elem in del_list: + self.dcnm_protocols_update_delete_payloads(elem) + + if cfg == []: + return + + if self.want: + # New configuration is included. Delete all existing PROTOCOLS objects and create new objects as requested + # through the configuration + self.dcnm_protocols_get_diff_merge() + + def dcnm_protocols_get_diff_deleted(self): + + """ + Routine to get a list of payload information that will be used to delete Protocols. + This routine updates self.diff_delete with payloads that are used to delete Protocols + from the server. + + Parameters: + None + + Returns: + None + """ + + if self.protocols_info == []: + # User has not included any config. Delete all existing PROTOCOLS objects from DCNM + self.dcnm_protocols_get_diff_overridden([]) + return + + for elem in self.protocols_info: + + # Perform any translations that may be required on the protocols_info. + have = dcnm_protocols_utils_get_protocols_info(self, elem) + + if have != {}: + self.dcnm_protocols_update_delete_payloads(have) + + def dcnm_protocols_update_delete_payloads(self, have): + + # Get the delete payload based on 'have' + del_payload = dcnm_protocols_utils_get_delete_payload(self, have) + + if del_payload != {} and del_payload not in self.diff_delete: + self.changed_dict[0]["deleted"].append( + del_payload + ) + self.diff_delete.append(del_payload) + + def dcnm_protocols_get_diff_merge(self): + + """ + Routine to populate a list of payload information in self.diff_create to create/update Protocols. + + Parameters: + None + + Returns: + None + """ + + if not self.want: + return + + for elem in self.want: + + rc, have = dcnm_protocols_utils_compare_want_and_have(self, elem) + + if rc == "NDFC_PROTOCOLS_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 == "NDFC_PROTOCOLS_MERGE": + # Object already exists, and needs an update + if elem not in self.diff_modify: + self.changed_dict[0]["modified"].append(elem) + + # Fields like CONF which are a list of commands should be handled differently in this case. + # For existing objects, we will have to merge the current list of commands with already existing + # ones in have. For replace, no need to merge them. They must be replaced with what is given. + if self.module.params["state"] == "merged": + self.dcnm_protocols_merge_want_and_have(elem, have) + self.diff_modify.append(elem) + + def dcnm_protocols_get_want(self): + + """ + This routine updates self.want with the payload information based on the playbook configuration. + + Parameters: + None + + Returns: + None + """ + + if [] is self.config: + return + + if not self.protocols_info: + return + + for elem in self.protocols_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_protocols_get_payload(elem) + if payload not in self.want: + self.want.append(payload) + + def dcnm_protocols_get_have(self): + + """ + Routine to get exisitng protocols information from DCNM that matches information in self.want. + This routine updates self.have with all the protocols that match the given playbook configuration + + Parameters: + None + + Returns: + None + """ + + if self.want == []: + return + + have = dcnm_protocols_utils_get_protocols_info(self, None) + if (have != []): + self.have = have + + def dcnm_protocols_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.protocols_info with + validated playbook information related to deleted state. + + Parameters: + cfg (dict): The config from playbook + + Returns: + None + """ + + arg_spec = {'protocol_name': {'type': 'str'}} + + protocols_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 protocols_info: + self.protocols_info.extend(protocols_info) + + def dcnm_protocols_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.protocols_info with + validated playbook information related to query state. + + Parameters: + cfg (dict): The config from playbook + + Returns: + None + """ + + arg_spec = {'protocol_name': {'type': 'str'}} + + protocols_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 protocols_info: + self.protocols_info.extend(protocols_info) + + def dcnm_protocols_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 = { + 'protocol_name': {'required': True, 'type': 'str'}, + 'description': {'type': 'str'}, + 'match_all': {'type': 'bool', 'default': False}, + 'match': {'type': 'list'} + } + + protocol_spec = { + 'type': {'required': True, 'type': 'str', 'choices': ['ip', 'ipv4', 'ipv6']}, + 'protocol_options': {'type': 'str', 'default': ""}, + 'fragments': {'type': 'bool', 'default': False}, + 'stateful': {'type': 'bool', 'default': False}, + 'source_port_range': {'type': 'str', 'default': ""}, + 'destination_port_range': {'type': 'str', 'default': ""}, + 'tcp_flags': {'type': 'str', 'choices': ['est', 'ack', 'fin', 'syn', 'rst', 'psh'], 'default': ""}, + 'dscp': {'type': 'int', 'range_min': 0, 'range_max': 63, 'default': None} + } + + protocols_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) + + for protocol in protocols_info: + if protocol.get("match"): + match_info, invalid_att = validate_list_of_dicts(protocol["match"], protocol_spec) + protocol["match"] = match_info + invalid_params.extend(invalid_att) + + if invalid_params: + mesg = "Invalid parameters in playbook: {0}".format(invalid_params) + self.module.fail_json(msg=mesg) + + for match in match_info: + if match.get("stateful") or match.get("tcp_flags"): + if match.get("protocol_options") and match.get("protocol_options").lower() != "tcp": + invalid_params.append("stateful/tcp_flags can be set only for TCP protocol") + + if match.get("fragments") or match.get("source_port_range") or match.get("destination_port_range"): + if match.get("protocol_options") and match.get("protocol_options").lower() not in ["tcp", "udp"]: + invalid_params.append("fragments/source_port_range/destination_port_range can be set only for TCP/UDP protocols") + + match["type"] = match["type"].lower() + + if match.get("protocol_options", None): + match["protocol_options"] = match["protocol_options"].lower() + else: + del match["protocol_options"] + + if match.get("tcp_flags", None): + match["tcp_flags"] = match["tcp_flags"].lower() + else: + del match["tcp_flags"] + + if not match.get("dscp", None): + del match["dscp"] + if not match.get("source_port_range", None): + del match["source_port_range"] + if not match.get("destination_port_range", None): + del match["destination_port_range"] + if not match.get("fragments", None): + del match["fragments"] + if not match.get("stateful", None): + del match["stateful"] + + if protocol.get("match_all"): + if protocol.get("match"): + invalid_params.append("match_all and match cannot be used together") + matchall = [{"type": "default"}] + protocol["match"] = matchall + + if invalid_params: + mesg = "Invalid parameters in playbook: {0}".format(invalid_params) + self.module.fail_json(msg=mesg) + + self.protocols_info.append(protocols_info[0]) + + def dcnm_protocols_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 [] is 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_protocols_validate_query_state_input(cfg) + elif self.module.params["state"] == "deleted": + # config for deleted state is different. So validate deleted state differently + self.dcnm_protocols_validate_deleted_state_input(cfg) + else: + self.dcnm_protocols_validate_input(cfg) + cfg.remove(citem) + + def dcnm_protocols_get_payload(self, protocols_info): + + """ + This routine builds the complete object payload based on the information in self.want + + Parameters: + protocols_info (dict): Object information + + Returns: + protocols_payload (dict): Object payload information populated with appropriate data from playbook config + """ + + protocols_payload = dcnm_protocols_utils_get_protocols_payload(self, protocols_info) + + return protocols_payload + + def dcnm_protocols_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 + + delete_flag = dcnm_protocols_utils_process_delete_payloads(self) + create_flag = dcnm_protocols_utils_process_create_payloads(self) + modify_flag = dcnm_protocols_utils_process_modify_payloads(self) + + self.result["changed"] = ( + create_flag or modify_flag or delete_flag + ) + + def dcnm_protocols_update_module_info(self): + + """ + Routine to update version and fabric details + + Parameters: + None + + Returns: + None + """ + + self.dcnm_version = dcnm_version_supported(self.module) + self.inventory_data = get_fabric_inventory_details( + self.module, self.fabric + ) + + self.fabric_info = get_fabric_details(self.module, self.fabric) + self.paths = dcnm_protocols_utils_get_paths(self.dcnm_version) + + +def main(): + + """ main entry point for module execution + """ + element_spec = dict( + fabric=dict(required=True, type="str"), + config=dict(required=False, type="list", elements="dict", default=[]), + state=dict( + type="str", + default="merged", + choices=["merged", "deleted", "replaced", "overridden", "query"], + ), + ) + + module = AnsibleModule( + argument_spec=element_spec, supports_check_mode=True + ) + + dcnm_protocols = DcnmProtocols(module) + + # Fill up the version and fabric related details + dcnm_protocols.dcnm_protocols_update_module_info() + + state = module.params["state"] + + if [] is dcnm_protocols.config: + if state == "merged" or state == "replaced": + module.fail_json( + msg="'config' element is mandatory for state '{0}', given = '{1}'".format( + state, dcnm_protocols.config + ) + ) + + dcnm_protocols.dcnm_protocols_validate_all_input() + + if ( + module.params["state"] != "query" and + module.params["state"] != "deleted" + ): + dcnm_protocols.dcnm_protocols_get_want() + + dcnm_protocols.dcnm_protocols_get_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. + + if (module.params["state"] == "merged") or ( + module.params["state"] == "replaced" + ): + dcnm_protocols.dcnm_protocols_get_diff_merge() + + if module.params["state"] == "deleted": + dcnm_protocols.dcnm_protocols_get_diff_deleted() + + if module.params["state"] == "overridden": + dcnm_protocols.dcnm_protocols_get_diff_overridden(dcnm_protocols.config) + + if module.params["state"] == "query": + dcnm_protocols.dcnm_protocols_get_diff_query() + + dcnm_protocols.result["diff"] = dcnm_protocols.changed_dict + + if dcnm_protocols.diff_create or dcnm_protocols.diff_delete: + dcnm_protocols.result["changed"] = True + + if module.check_mode: + dcnm_protocols.result["changed"] = False + module.exit_json(**dcnm_protocols.result) + + dcnm_protocols.dcnm_protocols_send_message_to_dcnm() + + module.exit_json(**dcnm_protocols.result) + + +if __name__ == "__main__": + main() diff --git a/tests/config.yaml b/tests/config.yml similarity index 100% rename from tests/config.yaml rename to tests/config.yml diff --git a/tests/integration/targets/dcnm_contracts/defaults/main.yaml b/tests/integration/targets/dcnm_contracts/defaults/main.yaml new file mode 100644 index 000000000..55a93fc23 --- /dev/null +++ b/tests/integration/targets/dcnm_contracts/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" \ No newline at end of file diff --git a/tests/integration/targets/dcnm_contracts/meta/main.yaml b/tests/integration/targets/dcnm_contracts/meta/main.yaml new file mode 100644 index 000000000..5514b6a40 --- /dev/null +++ b/tests/integration/targets/dcnm_contracts/meta/main.yaml @@ -0,0 +1 @@ +dependencies: [] \ No newline at end of file diff --git a/tests/integration/targets/dcnm_contracts/tasks/dcnm.yaml b/tests/integration/targets/dcnm_contracts/tasks/dcnm.yaml new file mode 100644 index 000000000..51516ad99 --- /dev/null +++ b/tests/integration/targets/dcnm_contracts/tasks/dcnm.yaml @@ -0,0 +1,32 @@ +--- +- 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 + +- name: Delete http and test protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted + config: + - protocol_name: test + - protocol_name: http diff --git a/tests/integration/targets/dcnm_contracts/tasks/main.yaml b/tests/integration/targets/dcnm_contracts/tasks/main.yaml new file mode 100644 index 000000000..96bde0c08 --- /dev/null +++ b/tests/integration/targets/dcnm_contracts/tasks/main.yaml @@ -0,0 +1,70 @@ +--- + +- set_fact: + controller_version: "Unable to determine controller version" + tags: sanity + +- name: Determine version of DCNM or NDFC + cisco.dcnm.dcnm_rest: + method: GET + path: /appcenter/cisco/ndfc/api/about/version + register: result + ignore_errors: yes + tags: sanity + +- set_fact: + controller_version: "{{ result.response['DATA']['version'][0:2] | int }}" + when: ( result.response['DATA']['version'] is search("\d\d.\d+") ) + ignore_errors: yes + tags: sanity + +- name: Determine version of DCNM or NDFC + cisco.dcnm.dcnm_rest: + method: GET + path: /fm/fmrest/about/version + register: result + ignore_errors: yes + tags: sanity + +- set_fact: + controller_version: "{{ result.response['DATA']['version'][0:2] | int }}" + when: ( result.response['DATA']['version'] is search("\d\d.\d+") ) + ignore_errors: yes + tags: sanity + +# No need to continue if we cannot determine the DCNM/NDFC controller version +- assert: + that: + - 'controller_version != "Unable to determine controller version"' + tags: sanity + +- name: Remove all existing contracts to start with a clean state + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: deleted + tags: sanity + +- name: Create http and test protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: merged + config: + - protocol_name: test + description: test + match: + - type: ip + protocol_options: "udp" + source_port_range: 10 + - type: ipv4 + protocol_options: "tcp" + destination_port_range: 20 + dscp: 10 + - protocol_name: http + description: http + match: + - type: ip + protocol_options: "tcp" + source_port_range: 80 + dscp: 10 + +- { include_tasks: dcnm.yaml, tags: ['dcnm'] } diff --git a/tests/integration/targets/dcnm_contracts/tests/dcnm/deleted.yaml b/tests/integration/targets/dcnm_contracts/tests/dcnm/deleted.yaml new file mode 100644 index 000000000..07939da69 --- /dev/null +++ b/tests/integration/targets/dcnm_contracts/tests/dcnm/deleted.yaml @@ -0,0 +1,161 @@ +############################################## +## SETUP ## +############################################## + +- set_fact: + rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ test_fabric }}" + +- name: DELETED - Verify if fabric - Fabric1 is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_path }}" + register: result + +- assert: + that: + - 'result.response.DATA != None' + +- name: DELETED - setup - Clean up any existing contracts + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: deleted + +############################################## +## DELETED ## +############################################## + +- name: DELETED - Create one contract + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: merged + config: + - contract_name: test + description: test + rules: + - direction: bidirectional + action: permit + protocol_name: http + - direction: unidirectional + action: permit + protocol_name: test + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: DELETED - Contract delete with config + cisco.dcnm.dcnm_contracts: &conf + fabric: "{{ test_fabric }}" + state: deleted + config: + - contract_name: test + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: DELETED - Idempotence + cisco.dcnm.dcnm_contracts: *conf + register: result + +- assert: + that: + - 'result.changed == false' + +- name: DELETED - Create multiple contract + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: merged + config: + - contract_name: test + description: test + rules: + - direction: bidirectional + action: permit + protocol_name: http + - direction: unidirectional + action: permit + protocol_name: test + - contract_name: test1 + description: test1 + rules: + - direction: bidirectional + action: permit + protocol_name: http + - direction: unidirectional + action: permit + protocol_name: test + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + - 'result.response[0].DATA.successList[1].name == "test1"' + +- name: DELETED - Contract delete one config + cisco.dcnm.dcnm_contracts: &conf1 + fabric: "{{ test_fabric }}" + state: deleted + config: + - contract_name: test + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: DELETED - Contract delete without config + cisco.dcnm.dcnm_contracts: &conf2 + fabric: "{{ test_fabric }}" + state: deleted + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test1"' + +- name: DELETED - Idempotence + cisco.dcnm.dcnm_contracts: *conf2 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: DELETED - Contract delete non existent config + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: deleted + config: + - contract_name: test + register: result + +- assert: + that: + - 'result.changed == false' + +############################################## +## CLEAN-UP ## +############################################## + +- name: DELETED - setup - remove any contracts + cisco.dcnm.dcnm_network: + fabric: "{{ test_fabric }}" + state: deleted \ No newline at end of file diff --git a/tests/integration/targets/dcnm_contracts/tests/dcnm/merged.yaml b/tests/integration/targets/dcnm_contracts/tests/dcnm/merged.yaml new file mode 100644 index 000000000..caa10faa2 --- /dev/null +++ b/tests/integration/targets/dcnm_contracts/tests/dcnm/merged.yaml @@ -0,0 +1,255 @@ +############################################## +## SETUP ## +############################################## + +- set_fact: + rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ test_fabric }}" + +- name: MERGED - Verify if fabric - Fabric1 is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_path }}" + register: result + +- assert: + that: + - 'result.response.DATA != None' + +- name: MERGED - setup - Clean up any existing contracts + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: deleted + +############################################## +## MERGED ## +############################################## + +- name: MERGED - Create one contract + cisco.dcnm.dcnm_contracts: &conf + fabric: "{{ test_fabric }}" + state: merged + config: + - contract_name: test + description: test + rules: + - direction: bidirectional + action: permit + protocol_name: http + - direction: unidirectional + action: permit + protocol_name: test + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: MERGED - conf - Idempotence + cisco.dcnm.dcnm_contracts: *conf + register: result + +- assert: + that: + - 'result.changed == false' + +- name: MERGED - Contract delete + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: deleted + +- name: MERGED - Create multiple contract + cisco.dcnm.dcnm_contracts: &conf1 + fabric: "{{ test_fabric }}" + state: merged + config: + - contract_name: test + description: test + rules: + - direction: bidirectional + action: permit + protocol_name: http + - direction: unidirectional + action: permit + protocol_name: test + - contract_name: test1 + description: test1 + rules: + - direction: bidirectional + action: permit + protocol_name: http + - direction: unidirectional + action: permit + protocol_name: test + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + - 'result.response[0].DATA.successList[1].name == "test1"' + +- name: MERGED - conf - Idempotence + cisco.dcnm.dcnm_contracts: *conf1 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: MERGED - Contract delete + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: deleted + +- name: MERGED - Create one contract + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: merged + config: + - contract_name: test + description: test + rules: + - direction: bidirectional + action: permit + protocol_name: http + - direction: unidirectional + action: permit + protocol_name: test + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: MERGED - Add another contract to existing contract + cisco.dcnm.dcnm_contracts: &conf2 + fabric: "{{ test_fabric }}" + state: merged + config: + - contract_name: test1 + description: test1 + rules: + - direction: bidirectional + action: permit + protocol_name: http + - direction: unidirectional + action: permit + protocol_name: test + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test1"' + +- name: MERGED - conf - Idempotence + cisco.dcnm.dcnm_contracts: *conf2 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: MERGED - Contract delete + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: deleted + +- name: MERGED - Create one contract + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: merged + config: + - contract_name: test + description: test + rules: + - direction: bidirectional + action: permit + protocol_name: http + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: MERGED - Add another entry to existing contract + cisco.dcnm.dcnm_contracts: &conf3 + fabric: "{{ test_fabric }}" + state: merged + config: + - contract_name: test + description: test + rules: + - direction: bidirectional + action: permit + protocol_name: http + - direction: unidirectional + action: permit + protocol_name: test + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].METHOD == "PUT"' + - 'result.response[0].DATA.name == "test"' + +- name: MERGED - conf - Idempotence + cisco.dcnm.dcnm_contracts: *conf3 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: MERGED - Contract delete + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: deleted + +- name: MERGED - Configure contract without mandatory param + cisco.dcnm.dcnm_contracts: + fabric: test_fab + state: merged + config: + - contract_name: test2 + description: test2 + rules: + - direction: bidirectional + action: permit + register: result + ignore_errors: yes + +- assert: + that: + - 'result.changed == false' + - '"protocol_name : Required parameter not found" in result.msg' + +- name: MERGED - Contract delete + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: deleted + +############################################## +## CLEAN-UP ## +############################################## + +- name: MERGED - setup - remove any contracts + cisco.dcnm.dcnm_network: + fabric: "{{ test_fabric }}" + state: deleted \ No newline at end of file diff --git a/tests/integration/targets/dcnm_contracts/tests/dcnm/overridden.yaml b/tests/integration/targets/dcnm_contracts/tests/dcnm/overridden.yaml new file mode 100644 index 000000000..3af3eba63 --- /dev/null +++ b/tests/integration/targets/dcnm_contracts/tests/dcnm/overridden.yaml @@ -0,0 +1,182 @@ +############################################## +## SETUP ## +############################################## + +- set_fact: + rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ test_fabric }}" + +- name: OVERRIDE - Verify if fabric - Fabric1 is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_path }}" + register: result + +- assert: + that: + - 'result.response.DATA != None' + +- name: OVERRIDE - setup - Clean up any existing contracts + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: deleted + +############################################## +## OVERRIDE ## +############################################## + +- name: OVERRIDE - Create one contract + cisco.dcnm.dcnm_contracts: &conf + fabric: "{{ test_fabric }}" + state: overridden + config: + - contract_name: test + description: test + rules: + - direction: bidirectional + action: permit + protocol_name: http + - direction: unidirectional + action: permit + protocol_name: test + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: OVERRIDE - Idempotence + cisco.dcnm.dcnm_contracts: *conf + register: result + +- assert: + that: + - 'result.changed == false' + +- name: OVERRIDE - Override rules in contract + cisco.dcnm.dcnm_contracts: &conf1 + fabric: "{{ test_fabric }}" + state: overridden + config: + - contract_name: test + description: test + rules: + - direction: bidirectional + action: permit + protocol_name: http + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].METHOD == "PUT"' + - 'result.response[0].DATA.name == "test"' + +- name: OVERRIDE - Idempotence + cisco.dcnm.dcnm_contracts: *conf1 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: OVERRIDE - Create another contract + cisco.dcnm.dcnm_contracts: &conf2 + fabric: "{{ test_fabric }}" + state: overridden + config: + - contract_name: test1 + description: test1 + rules: + - direction: bidirectional + action: permit + protocol_name: http + - direction: unidirectional + action: permit + protocol_name: test + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[1].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[1].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + - 'result.response[1].DATA.successList[0].name == "test1"' + +- name: OVERRIDE - Idempotence + cisco.dcnm.dcnm_contracts: *conf2 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: OVERRIDE - Contract query + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: query + register: result + +- assert: + that: + - 'result.changed == false' + - "result.response | length == 1" + +- name: OVERRIDE - Contract delete config + cisco.dcnm.dcnm_contracts: &conf3 + fabric: "{{ test_fabric }}" + state: overridden + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + +- name: OVERRIDE - Idempotence + cisco.dcnm.dcnm_contracts: *conf3 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: OVERRIDE - Configure contract without mandatory param + cisco.dcnm.dcnm_contracts: + fabric: test_fab + state: overridden + config: + - contract_name: test2 + description: test2 + rules: + - direction: bidirectional + action: permit + register: result + ignore_errors: yes + +- assert: + that: + - 'result.changed == false' + - '"protocol_name : Required parameter not found" in result.msg' + +- name: OVERRIDE - Contract delete + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: deleted + +############################################## +## CLEAN-UP ## +############################################## + +- name: OVERRIDE - setup - remove any contracts + cisco.dcnm.dcnm_network: + fabric: "{{ test_fabric }}" + state: deleted diff --git a/tests/integration/targets/dcnm_contracts/tests/dcnm/query.yaml b/tests/integration/targets/dcnm_contracts/tests/dcnm/query.yaml new file mode 100644 index 000000000..8cf8f69bc --- /dev/null +++ b/tests/integration/targets/dcnm_contracts/tests/dcnm/query.yaml @@ -0,0 +1,154 @@ +############################################## +## SETUP ## +############################################## + +- set_fact: + rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ test_fabric }}" + +- name: QUERY - Verify if fabric - Fabric1 is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_path }}" + register: result + +- assert: + that: + - 'result.response.DATA != None' + +- name: QUERY - setup - Clean up any existing contracts + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: deleted + +############################################## +## QUERY ## +############################################## + +- name: QUERY - Create one contract + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: merged + config: + - contract_name: test + description: test + rules: + - direction: bidirectional + action: permit + protocol_name: http + - direction: unidirectional + action: permit + protocol_name: test + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: QUERY - Contract query with config + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: query + config: + - contract_name: test + register: result + +- assert: + that: + - 'result.changed == false' + - 'result.response[0].contractName == "test"' + - "result.response | length == 1" + +- name: QUERY - Contract delete + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: deleted + +- name: QUERY - Create multiple contract + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: merged + config: + - contract_name: test + description: test + rules: + - direction: bidirectional + action: permit + protocol_name: http + - direction: unidirectional + action: permit + protocol_name: test + - contract_name: test1 + description: test1 + rules: + - direction: bidirectional + action: permit + protocol_name: http + - direction: unidirectional + action: permit + protocol_name: test + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + - 'result.response[0].DATA.successList[1].name == "test1"' + +- name: QUERY - Contract query with config + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: query + config: + - contract_name: test + register: result + +- assert: + that: + - 'result.changed == false' + - 'result.response[0].contractName == "test"' + - "result.response | length == 1" + +- name: QUERY - Contract query with no config + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: query + register: result + +- assert: + that: + - 'result.changed == false' + - 'result.response[0].contractName == "test"' + - 'result.response[1].contractName == "test1"' + - "result.response | length == 2" + +- name: QUERY - Contract delete + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: deleted + +- name: QUERY - Contract query a non-existent contract + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: query + config: + - contract_name: test + register: result + +- assert: + that: + - 'result.changed == false' + - "result.response | length == 0" + +############################################## +## CLEAN-UP ## +############################################## + +- name: QUERY - setup - remove any contracts + cisco.dcnm.dcnm_network: + fabric: "{{ test_fabric }}" + state: deleted \ No newline at end of file diff --git a/tests/integration/targets/dcnm_contracts/tests/dcnm/replaced.yaml b/tests/integration/targets/dcnm_contracts/tests/dcnm/replaced.yaml new file mode 100644 index 000000000..4f69b1fd3 --- /dev/null +++ b/tests/integration/targets/dcnm_contracts/tests/dcnm/replaced.yaml @@ -0,0 +1,164 @@ +############################################## +## SETUP ## +############################################## + +- set_fact: + rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ test_fabric }}" + +- name: REPLACED - Verify if fabric - Fabric1 is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_path }}" + register: result + +- assert: + that: + - 'result.response.DATA != None' + +- name: REPLACED - setup - Clean up any existing contracts + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: deleted + +############################################## +## REPLACED ## +############################################## + +- name: REPLACED - Create one contract + cisco.dcnm.dcnm_contracts: &conf + fabric: "{{ test_fabric }}" + state: replaced + config: + - contract_name: test + description: test + rules: + - direction: bidirectional + action: permit + protocol_name: http + - direction: unidirectional + action: permit + protocol_name: test + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: REPLACED - Idempotence + cisco.dcnm.dcnm_contracts: *conf + register: result + +- assert: + that: + - 'result.changed == false' + +- name: REPLACED - replace rules in contract + cisco.dcnm.dcnm_contracts: &conf1 + fabric: "{{ test_fabric }}" + state: replaced + config: + - contract_name: test + description: test + rules: + - direction: bidirectional + action: permit + protocol_name: http + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].METHOD == "PUT"' + - 'result.response[0].DATA.name == "test"' + +- name: REPLACED - Idempotence + cisco.dcnm.dcnm_contracts: *conf1 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: REPLACED - Create another contract + cisco.dcnm.dcnm_contracts: &conf2 + fabric: "{{ test_fabric }}" + state: replaced + config: + - contract_name: test1 + description: test1 + rules: + - direction: bidirectional + action: permit + protocol_name: http + - direction: unidirectional + action: permit + protocol_name: test + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test1"' + +- name: REPLACED - Idempotence + cisco.dcnm.dcnm_contracts: *conf2 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: REPLACED - Contract query + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: query + register: result + +- assert: + that: + - 'result.changed == false' + - "result.response | length == 2" + +- name: REPLACED - Contract delete config + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: deleted + +- name: REPLACED - Configure contract without mandatory param + cisco.dcnm.dcnm_contracts: + fabric: test_fab + state: replaced + config: + - contract_name: test2 + description: test2 + rules: + - direction: bidirectional + action: permit + register: result + ignore_errors: yes + +- assert: + that: + - 'result.changed == false' + - '"protocol_name : Required parameter not found" in result.msg' + +- name: REPLACED - Contract delete + cisco.dcnm.dcnm_contracts: + fabric: "{{ test_fabric }}" + state: deleted + +############################################## +## CLEAN-UP ## +############################################## + +- name: REPLACED - setup - remove any contracts + cisco.dcnm.dcnm_network: + fabric: "{{ test_fabric }}" + state: deleted diff --git a/tests/integration/targets/dcnm_protocols/defaults/main.yaml b/tests/integration/targets/dcnm_protocols/defaults/main.yaml new file mode 100644 index 000000000..55a93fc23 --- /dev/null +++ b/tests/integration/targets/dcnm_protocols/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" \ No newline at end of file diff --git a/tests/integration/targets/dcnm_protocols/meta/main.yaml b/tests/integration/targets/dcnm_protocols/meta/main.yaml new file mode 100644 index 000000000..5514b6a40 --- /dev/null +++ b/tests/integration/targets/dcnm_protocols/meta/main.yaml @@ -0,0 +1 @@ +dependencies: [] \ No newline at end of file diff --git a/tests/integration/targets/dcnm_protocols/tasks/dcnm.yaml b/tests/integration/targets/dcnm_protocols/tasks/dcnm.yaml new file mode 100644 index 000000000..57324e706 --- /dev/null +++ b/tests/integration/targets/dcnm_protocols/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_protocols/tasks/main.yaml b/tests/integration/targets/dcnm_protocols/tasks/main.yaml new file mode 100644 index 000000000..4996fedfb --- /dev/null +++ b/tests/integration/targets/dcnm_protocols/tasks/main.yaml @@ -0,0 +1,47 @@ +--- + +- set_fact: + controller_version: "Unable to determine controller version" + tags: sanity + +- name: Determine version of DCNM or NDFC + cisco.dcnm.dcnm_rest: + method: GET + path: /appcenter/cisco/ndfc/api/about/version + register: result + ignore_errors: yes + tags: sanity + +- set_fact: + controller_version: "{{ result.response['DATA']['version'][0:2] | int }}" + when: ( result.response['DATA']['version'] is search("\d\d.\d+") ) + ignore_errors: yes + tags: sanity + +- name: Determine version of DCNM or NDFC + cisco.dcnm.dcnm_rest: + method: GET + path: /fm/fmrest/about/version + register: result + ignore_errors: yes + tags: sanity + +- set_fact: + controller_version: "{{ result.response['DATA']['version'][0:2] | int }}" + when: ( result.response['DATA']['version'] is search("\d\d.\d+") ) + ignore_errors: yes + tags: sanity + +# No need to continue if we cannot determine the DCNM/NDFC controller version +- assert: + that: + - 'controller_version != "Unable to determine controller version"' + tags: sanity + +- name: Remove all existing protocols to start with a clean state + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted + tags: sanity + +- { include_tasks: dcnm.yaml, tags: ['dcnm'] } diff --git a/tests/integration/targets/dcnm_protocols/tests/dcnm/deleted.yaml b/tests/integration/targets/dcnm_protocols/tests/dcnm/deleted.yaml new file mode 100644 index 000000000..3726087ee --- /dev/null +++ b/tests/integration/targets/dcnm_protocols/tests/dcnm/deleted.yaml @@ -0,0 +1,172 @@ +############################################## +## SETUP ## +############################################## + +- set_fact: + rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ test_fabric }}" + +- name: DELETED - Verify if fabric - Fabric1 is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_path }}" + register: result + +- assert: + that: + - 'result.response.DATA != None' + +- name: DELETED - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted + +############################################## +## DELETED ## +############################################## + +- name: DELETED - Create one protocol + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: merged + config: + - protocol_name: test + description: test + match: + - type: ip + protocol_options: "udp" + source_port_range: 10 + - type: ipv4 + protocol_options: "tcp" + destination_port_range: 20 + dscp: 10 + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: DELETED - Protocol delete with config + cisco.dcnm.dcnm_protocols: &conf + fabric: "{{ test_fabric }}" + state: deleted + config: + - protocol_name: test + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: DELETED - Idempotence + cisco.dcnm.dcnm_protocols: *conf + register: result + +- assert: + that: + - 'result.changed == false' + +- name: DELETED - Create multiple protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: merged + config: + - protocol_name: test + description: test + match: + - type: ip + protocol_options: "udp" + source_port_range: 10 + - type: ipv4 + protocol_options: "tcp" + destination_port_range: 20 + dscp: 10 + - protocol_name: test1 + description: test1 + match: + - type: ipv6 + protocol_options: "icmpv6" + dscp: 10 + - protocol_name: test2 + description: test2 + match_all: True + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + - 'result.response[0].DATA.successList[1].name == "test1"' + - 'result.response[0].DATA.successList[2].name == "test2"' + +- name: DELETED - Protocol delete one with config + cisco.dcnm.dcnm_protocols: &conf1 + fabric: "{{ test_fabric }}" + state: deleted + config: + - protocol_name: test + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: DELETED - Idempotence + cisco.dcnm.dcnm_protocols: *conf1 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: DELETED - Protocol delete without config + cisco.dcnm.dcnm_protocols: &conf2 + fabric: "{{ test_fabric }}" + state: deleted + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test1"' + - 'result.response[0].DATA.successList[1].name == "test2"' + +- name: DELETED - Idempotence + cisco.dcnm.dcnm_protocols: *conf2 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: DELETED - Protocol delete non existent config + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted + config: + - protocol_name: test_ansible + register: result + +- assert: + that: + - 'result.changed == false' + +############################################## +## CLEAN-UP ## +############################################## + +- name: DELETED - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted diff --git a/tests/integration/targets/dcnm_protocols/tests/dcnm/merged.yaml b/tests/integration/targets/dcnm_protocols/tests/dcnm/merged.yaml new file mode 100644 index 000000000..c165907fa --- /dev/null +++ b/tests/integration/targets/dcnm_protocols/tests/dcnm/merged.yaml @@ -0,0 +1,260 @@ +############################################## +## SETUP ## +############################################## + +- set_fact: + rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ test_fabric }}" + +- name: MERGED - Verify if fabric - Fabric1 is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_path }}" + register: result + +- assert: + that: + - 'result.response.DATA != None' + +- name: MERGED - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted + +############################################## +## MERGED ## +############################################## + +- name: MERGED - Create one protocol + cisco.dcnm.dcnm_protocols: &conf + fabric: "{{ test_fabric }}" + state: merged + config: + - protocol_name: test + description: test + match: + - type: ip + protocol_options: "udp" + source_port_range: 10 + - type: ipv4 + protocol_options: "tcp" + destination_port_range: 20 + dscp: 10 + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: MERGED - conf - Idempotence + cisco.dcnm.dcnm_protocols: *conf + register: result + +- assert: + that: + - 'result.changed == false' + +- name: MERGED - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted + +- name: MERGED - Create multiple protocols + cisco.dcnm.dcnm_protocols: &conf1 + fabric: "{{ test_fabric }}" + state: merged + config: + - protocol_name: test + description: test + match: + - type: ip + protocol_options: "udp" + source_port_range: 10 + - type: ipv4 + protocol_options: "tcp" + destination_port_range: 20 + dscp: 10 + - protocol_name: test1 + description: test1 + match: + - type: ipv6 + protocol_options: "icmpv6" + dscp: 10 + - protocol_name: test2 + description: test2 + match_all: True + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + - 'result.response[0].DATA.successList[1].name == "test1"' + - 'result.response[0].DATA.successList[2].name == "test2"' + +- name: MERGED - conf - Idempotence + cisco.dcnm.dcnm_protocols: *conf1 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: MERGED - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted + +- name: MERGED - Create one protocol + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: merged + config: + - protocol_name: test + description: test + match: + - type: ip + protocol_options: "udp" + source_port_range: 10 + - type: ipv4 + protocol_options: "tcp" + destination_port_range: 20 + dscp: 10 + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: MERGED - Create another protocol with existing one + cisco.dcnm.dcnm_protocols: &conf2 + fabric: "{{ test_fabric }}" + state: merged + config: + - protocol_name: test1 + description: test1 + match: + - type: ip + protocol_options: "udp" + source_port_range: 10 + - type: ipv4 + protocol_options: "tcp" + destination_port_range: 20 + dscp: 10 + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test1"' + +- name: MERGED - conf - Idempotence + cisco.dcnm.dcnm_protocols: *conf2 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: MERGED - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted + +- name: MERGED - Create one protocol + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: merged + config: + - protocol_name: test + description: test + match: + - type: ip + protocol_options: "udp" + source_port_range: 10 + - type: ipv4 + protocol_options: "tcp" + destination_port_range: 20 + dscp: 10 + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: MERGED - Add a new match to existing protocol + cisco.dcnm.dcnm_protocols: &conf3 + fabric: "{{ test_fabric }}" + state: merged + config: + - protocol_name: test + description: test + match: + - type: ip + protocol_options: "icmp" + dscp: 40 + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].METHOD == "PUT"' + - 'result.response[0].DATA.name == "test"' + +- name: MERGED - conf - Idempotence + cisco.dcnm.dcnm_protocols: *conf3 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: MERGED - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted + +- name: MERGED - Create a protocol without mandatory fields + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: merged + config: + - protocol_name: test1 + description: test1 + match: + - protocol_options: "udp" + source_port_range: 10 + - type: ipv4 + protocol_options: "tcp" + destination_port_range: 20 + dscp: 10 + register: result + ignore_errors: yes + +- assert: + that: + - 'result.changed == false' + - '"type : Required parameter not found" in result.msg' + +############################################## +## CLEAN-UP ## +############################################## + +- name: MERGED - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted \ No newline at end of file diff --git a/tests/integration/targets/dcnm_protocols/tests/dcnm/overridden.yaml b/tests/integration/targets/dcnm_protocols/tests/dcnm/overridden.yaml new file mode 100644 index 000000000..ee074b19f --- /dev/null +++ b/tests/integration/targets/dcnm_protocols/tests/dcnm/overridden.yaml @@ -0,0 +1,171 @@ +############################################## +## SETUP ## +############################################## + +- set_fact: + rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ test_fabric }}" + +- name: OVERRIDE - Verify if fabric - Fabric1 is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_path }}" + register: result + +- assert: + that: + - 'result.response.DATA != None' + +- name: OVERRIDE - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted + +############################################## +## OVERRIDE ## +############################################## + +- name: OVERRIDE - Create one protocol + cisco.dcnm.dcnm_protocols: &conf + fabric: "{{ test_fabric }}" + state: overridden + config: + - protocol_name: test + description: test + match: + - type: ip + protocol_options: "udp" + source_port_range: 10 + - type: ipv4 + protocol_options: "tcp" + destination_port_range: 20 + dscp: 10 + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: OVERRIDE - conf - Idempotence + cisco.dcnm.dcnm_protocols: *conf + register: result + +- assert: + that: + - 'result.changed == false' + +- name: OVERRIDE - override exiting match in protocol + cisco.dcnm.dcnm_protocols: &conf1 + fabric: "{{ test_fabric }}" + state: overridden + config: + - protocol_name: test + description: test + match: + - type: ip + protocol_options: "tcp" + source_port_range: 10 + - type: ipv4 + protocol_options: "icmp" + dscp: 10 + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].METHOD == "PUT"' + - 'result.response[0].DATA.name == "test"' + +- name: OVERRIDE - conf - Idempotence + cisco.dcnm.dcnm_protocols: *conf1 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: OVERRIDE - Create another protocol + cisco.dcnm.dcnm_protocols: &conf2 + fabric: "{{ test_fabric }}" + state: overridden + config: + - protocol_name: test1 + description: test1 + match: + - type: ip + protocol_options: "udp" + source_port_range: 110 + - type: ipv4 + protocol_options: "tcp" + destination_port_range: 40 + dscp: 32 + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[1].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[1].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + - 'result.response[1].DATA.successList[0].name == "test1"' + +- name: OVERRIDE - conf - Idempotence + cisco.dcnm.dcnm_protocols: *conf2 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: OVERRIDE - Protocols query + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: query + register: result + +- assert: + that: + - 'result.changed == false' + - "result.response | length == 1" + +- name: OVERRIDE - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted + +- name: OVERRIDE - Create a protocol without mandatory fields + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: overridden + config: + - protocol_name: test1 + description: test1 + match: + - protocol_options: "udp" + source_port_range: 10 + - type: ipv4 + protocol_options: "tcp" + destination_port_range: 20 + dscp: 10 + register: result + ignore_errors: yes + +- assert: + that: + - 'result.changed == false' + - '"type : Required parameter not found" in result.msg' + +############################################## +## CLEAN-UP ## +############################################## + +- name: OVERRIDE - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted \ No newline at end of file diff --git a/tests/integration/targets/dcnm_protocols/tests/dcnm/query.yaml b/tests/integration/targets/dcnm_protocols/tests/dcnm/query.yaml new file mode 100644 index 000000000..88e2dd887 --- /dev/null +++ b/tests/integration/targets/dcnm_protocols/tests/dcnm/query.yaml @@ -0,0 +1,158 @@ +############################################## +## SETUP ## +############################################## + +- set_fact: + rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ test_fabric }}" + +- name: QUERY - Verify if fabric - Fabric1 is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_path }}" + register: result + +- assert: + that: + - 'result.response.DATA != None' + +- name: QUERY - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted + +############################################## +## QUERY ## +############################################## + +- name: QUERY - Create one protocol + cisco.dcnm.dcnm_protocols: &conf + fabric: "{{ test_fabric }}" + state: merged + config: + - protocol_name: test + description: test + match: + - type: ip + protocol_options: "udp" + source_port_range: 10 + - type: ipv4 + protocol_options: "tcp" + destination_port_range: 20 + dscp: 10 + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: QUERY - Protocol query with config + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: query + config: + - protocol_name: test + register: result + +- assert: + that: + - 'result.changed == false' + - 'result.response[0].protocolName == "test"' + - "result.response | length == 1" + +- name: QUERY - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted + +- name: QUERY - Create multiple protocols + cisco.dcnm.dcnm_protocols: &conf1 + fabric: "{{ test_fabric }}" + state: merged + config: + - protocol_name: test + description: test + match: + - type: ip + protocol_options: "udp" + source_port_range: 10 + - type: ipv4 + protocol_options: "tcp" + destination_port_range: 20 + dscp: 10 + - protocol_name: test1 + description: test1 + match: + - type: ipv6 + protocol_options: "icmpv6" + dscp: 10 + - protocol_name: test2 + description: test2 + match_all: True + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + - 'result.response[0].DATA.successList[1].name == "test1"' + - 'result.response[0].DATA.successList[2].name == "test2"' + +- name: QUERY - Protocol query with config + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: query + config: + - protocol_name: test + register: result + +- assert: + that: + - 'result.changed == false' + - 'result.response[0].protocolName == "test"' + - "result.response | length == 1" + +- name: QUERY - Protocol query with no config + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: query + register: result + +- assert: + that: + - 'result.changed == false' + - '"test" or "test1" or "test2" in result.response[0].protocolName' + - '"test" or "test1" or "test2" in result.response[1].protocolName' + - '"test" or "test1" or "test2" in result.response[2].protocolName' + - "result.response | length == 3" + +- name: QUERY - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted + +- name: QUERY - Protocol query non existent config + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: query + config: + - protocol_name: test + register: result + +- assert: + that: + - 'result.changed == false' + - "result.response | length == 0" + +############################################## +## CLEAN-UP ## +############################################## + +- name: QUERY - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted \ No newline at end of file diff --git a/tests/integration/targets/dcnm_protocols/tests/dcnm/replaced.yaml b/tests/integration/targets/dcnm_protocols/tests/dcnm/replaced.yaml new file mode 100644 index 000000000..7ee9c9c88 --- /dev/null +++ b/tests/integration/targets/dcnm_protocols/tests/dcnm/replaced.yaml @@ -0,0 +1,168 @@ +############################################## +## SETUP ## +############################################## + +- set_fact: + rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ test_fabric }}" + +- name: REPLACED - Verify if fabric - Fabric1 is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_path }}" + register: result + +- assert: + that: + - 'result.response.DATA != None' + +- name: REPLACED - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted + +############################################## +## REPLACED ## +############################################## + +- name: REPLACED - Create one protocol + cisco.dcnm.dcnm_protocols: &conf + fabric: "{{ test_fabric }}" + state: replaced + config: + - protocol_name: test + description: test + match: + - type: ip + protocol_options: "udp" + source_port_range: 10 + - type: ipv4 + protocol_options: "tcp" + destination_port_range: 20 + dscp: 10 + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test"' + +- name: REPLACED - conf - Idempotence + cisco.dcnm.dcnm_protocols: *conf + register: result + +- assert: + that: + - 'result.changed == false' + +- name: REPLACED - replaced exiting match in protocol + cisco.dcnm.dcnm_protocols: &conf1 + fabric: "{{ test_fabric }}" + state: replaced + config: + - protocol_name: test + description: test + match: + - type: ip + protocol_options: "tcp" + source_port_range: 10 + - type: ipv4 + protocol_options: "icmp" + dscp: 10 + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].METHOD == "PUT"' + - 'result.response[0].DATA.name == "test"' + +- name: REPLACED - conf - Idempotence + cisco.dcnm.dcnm_protocols: *conf1 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: REPLACED - Create another protocol + cisco.dcnm.dcnm_protocols: &conf2 + fabric: "{{ test_fabric }}" + state: replaced + config: + - protocol_name: test1 + description: test1 + match: + - type: ip + protocol_options: "udp" + source_port_range: 110 + - type: ipv4 + protocol_options: "tcp" + destination_port_range: 40 + dscp: 32 + register: result + +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[0].MESSAGE == "OK"' + - 'result.response[0].DATA.successList[0].name == "test1"' + +- name: REPLACED - conf - Idempotence + cisco.dcnm.dcnm_protocols: *conf2 + register: result + +- assert: + that: + - 'result.changed == false' + +- name: REPLACED - Protocols query + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: query + register: result + +- assert: + that: + - 'result.changed == false' + - "result.response | length == 2" + +- name: REPLACED - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted + +- name: REPLACED - Create a protocol without mandatory fields + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: replaced + config: + - protocol_name: test1 + description: test1 + match: + - protocol_options: "udp" + source_port_range: 10 + - type: ipv4 + protocol_options: "tcp" + destination_port_range: 20 + dscp: 10 + register: result + ignore_errors: yes + +- assert: + that: + - 'result.changed == false' + - '"type : Required parameter not found" in result.msg' + +############################################## +## CLEAN-UP ## +############################################## + +- name: REPLACED - setup - Clean up any existing protocols + cisco.dcnm.dcnm_protocols: + fabric: "{{ test_fabric }}" + state: deleted \ No newline at end of file diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index 60d9043d3..2ad26a9aa 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -16,5 +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_contracts.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_protocols.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..378e14202 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -16,6 +16,8 @@ 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_contracts.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_protocols.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..3e1cca9d3 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -16,6 +16,8 @@ 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_contracts.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_protocols.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..33c823a3d 100644 --- a/tests/sanity/ignore-2.13.txt +++ b/tests/sanity/ignore-2.13.txt @@ -17,6 +17,8 @@ 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_contracts.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_protocols.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..a66261d9e 100644 --- a/tests/sanity/ignore-2.14.txt +++ b/tests/sanity/ignore-2.14.txt @@ -17,6 +17,8 @@ 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_contracts.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_protocols.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..a66261d9e 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -17,6 +17,8 @@ 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_contracts.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_protocols.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..31ced34e5 100644 --- a/tests/sanity/ignore-2.16.txt +++ b/tests/sanity/ignore-2.16.txt @@ -17,3 +17,5 @@ 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_contracts.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module +plugins/modules/dcnm_protocols.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_contracts/dcnm_contracts_common.py b/tests/unit/modules/dcnm/fixtures/dcnm_contracts/dcnm_contracts_common.py new file mode 100644 index 000000000..53cb2d125 --- /dev/null +++ b/tests/unit/modules/dcnm/fixtures/dcnm_contracts/dcnm_contracts_common.py @@ -0,0 +1,55 @@ +# 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_contracts import ( + DcnmContracts, +) + + +class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + + params = { + "config": [], + "state": "merged", + "fabric": "test_fab", + } + supports_check_mode = True + + @staticmethod + def fail_json(msg, **kwargs) -> AnsibleFailJson: + """ + mock the fail_json method + """ + raise AnsibleFailJson(msg, kwargs) + + +@pytest.fixture(name="dcnm_contracts_fixture") +def dcnm_contracts_fixture(monkeypatch): + """ + mock DcnmContracts + """ + + return DcnmContracts(MockAnsibleModule) diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_contracts/dcnm_contracts_data.json b/tests/unit/modules/dcnm/fixtures/dcnm_contracts/dcnm_contracts_data.json new file mode 100644 index 000000000..4ad0eba26 --- /dev/null +++ b/tests/unit/modules/dcnm/fixtures/dcnm_contracts/dcnm_contracts_data.json @@ -0,0 +1,44 @@ +{ + "contracts_cfg_00001": + { + "contract_name": "test", + "description": "test", + "rules": [ + { + "direction": "bidirectional", + "action": "permit", + "protocol_name": "http" + } + ] + }, + + "contracts_cfg_00002": + { + "contract_name": "test", + "description": "test", + "rules": [ + { + "direction": "bidirectional", + "action": "permit", + "protocol_name": "http" + }, + { + "direction": "bidirectional", + "action": "permit", + "protocol_name": "test1" + } + ] + }, + + "contracts_cfg_00003": { + "contract_name": "test", + "description": "test", + "rules": [ + { + "direction": "bidirectional", + "action": "deny", + "protocol_name": "test" + } + ] + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_contracts/dcnm_contracts_response.json b/tests/unit/modules/dcnm/fixtures/dcnm_contracts/dcnm_contracts_response.json new file mode 100644 index 000000000..ee157d19b --- /dev/null +++ b/tests/unit/modules/dcnm/fixtures/dcnm_contracts/dcnm_contracts_response.json @@ -0,0 +1,809 @@ +{ + "contracts_inv_details": { + "192.168.2.1": { + "switchRoleEnum": "Leaf", + "vrf": "management", + "fabricTechnology": "VXLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 6, + "name": null, + "domainID": 0, + "wwn": null, + "membership": null, + "ports": 0, + "model": "N9K-C9300v", + "version": null, + "upTime": 0, + "ipAddress": "192.168.2.1", + "mgmtAddress": null, + "vendor": "Cisco", + "displayHdrs": null, + "displayValues": null, + "colDBId": 0, + "fid": 0, + "isLan": false, + "is_smlic_enabled": false, + "present": true, + "licenseViolation": false, + "managable": true, + "mds": false, + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "test_netv2", + "modelType": 0, + "logicalName": "leaf1", + "switchDbID": 86340, + "uid": 0, + "release": "10.3(1)", + "location": null, + "contact": null, + "upTimeStr": "21 days, 01:17:37", + "upTimeNumber": 0, + "network": null, + "nonMdsModel": null, + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": null, + "vsanWwnName": null, + "swWwn": null, + "swWwnName": null, + "serialNumber": "9SFRKD0M6AS", + "domain": null, + "principal": null, + "status": "ok", + "index": 0, + "licenseDetail": null, + "isPmCollect": false, + "sanAnalyticsCapable": false, + "vdcId": 0, + "vdcName": "", + "vdcMac": null, + "fcoeEnabled": false, + "cpuUsage": 0, + "memoryUsage": 0, + "scope": null, + "fex": false, + "health": -1, + "npvEnabled": false, + "linkName": null, + "username": null, + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": false, + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": false, + "switchRole": "leaf", + "mode": "Normal", + "hostName": "leaf1", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": null, + "operMode": null, + "modules": null, + "fexMap": {}, + "isVpcConfigured": false, + "vpcDomain": 0, + "role": null, + "peer": null, + "peerSerialNumber": null, + "peerSwitchDbId": 0, + "peerlinkState": null, + "keepAliveState": null, + "consistencyState": false, + "sendIntf": null, + "recvIntf": null, + "interfaces": null, + "elementType": null, + "monitorMode": null, + "freezeMode": null, + "cfsSyslogStatus": 1, + "isNonNexus": false, + "swUUIDId": 86370, + "swUUID": "DCNM-UUID-86370", + "swType": null, + "ccStatus": "In-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + }, + "192.168.2.2": { + "switchRoleEnum": "Leaf", + "vrf": "management", + "fabricTechnology": "VXLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 6, + "name": null, + "domainID": 0, + "wwn": null, + "membership": null, + "ports": 0, + "model": "N9K-C9300v", + "version": null, + "upTime": 0, + "ipAddress": "192.168.2.2", + "mgmtAddress": null, + "vendor": "Cisco", + "displayHdrs": null, + "displayValues": null, + "colDBId": 0, + "fid": 0, + "isLan": false, + "is_smlic_enabled": false, + "present": true, + "licenseViolation": false, + "managable": true, + "mds": false, + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "test_netv2", + "modelType": 0, + "logicalName": "leaf2", + "switchDbID": 86290, + "uid": 0, + "release": "10.3(1)", + "location": null, + "contact": null, + "upTimeStr": "21 days, 01:17:32", + "upTimeNumber": 0, + "network": null, + "nonMdsModel": null, + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": null, + "vsanWwnName": null, + "swWwn": null, + "swWwnName": null, + "serialNumber": "9KRDG57QQZT", + "domain": null, + "principal": null, + "status": "ok", + "index": 0, + "licenseDetail": null, + "isPmCollect": false, + "sanAnalyticsCapable": false, + "vdcId": 0, + "vdcName": "", + "vdcMac": null, + "fcoeEnabled": false, + "cpuUsage": 0, + "memoryUsage": 0, + "scope": null, + "fex": false, + "health": -1, + "npvEnabled": false, + "linkName": null, + "username": null, + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": false, + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": false, + "switchRole": "leaf", + "mode": "Normal", + "hostName": "leaf2", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": null, + "operMode": null, + "modules": null, + "fexMap": {}, + "isVpcConfigured": false, + "vpcDomain": 0, + "role": null, + "peer": null, + "peerSerialNumber": null, + "peerSwitchDbId": 0, + "peerlinkState": null, + "keepAliveState": null, + "consistencyState": false, + "sendIntf": null, + "recvIntf": null, + "interfaces": null, + "elementType": null, + "monitorMode": null, + "freezeMode": null, + "cfsSyslogStatus": 1, + "isNonNexus": false, + "swUUIDId": 86320, + "swUUID": "DCNM-UUID-86320", + "swType": null, + "ccStatus": "In-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + }, + "192.168.2.3": { + "switchRoleEnum": "Tor", + "vrf": "management", + "fabricTechnology": "VXLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 6, + "name": null, + "domainID": 0, + "wwn": null, + "membership": null, + "ports": 0, + "model": "N9K-C9300v", + "version": null, + "upTime": 0, + "ipAddress": "192.168.2.3", + "mgmtAddress": null, + "vendor": "Cisco", + "displayHdrs": null, + "displayValues": null, + "colDBId": 0, + "fid": 0, + "isLan": false, + "is_smlic_enabled": false, + "present": true, + "licenseViolation": false, + "managable": true, + "mds": false, + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "test_netv2", + "modelType": 0, + "logicalName": "tor", + "switchDbID": 101110, + "uid": 0, + "release": "10.3(1)", + "location": null, + "contact": null, + "upTimeStr": "21 days, 00:27:44", + "upTimeNumber": 0, + "network": null, + "nonMdsModel": null, + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": null, + "vsanWwnName": null, + "swWwn": null, + "swWwnName": null, + "serialNumber": "959A4D0NYXI", + "domain": null, + "principal": null, + "status": "ok", + "index": 0, + "licenseDetail": null, + "isPmCollect": false, + "sanAnalyticsCapable": false, + "vdcId": 0, + "vdcName": "", + "vdcMac": null, + "fcoeEnabled": false, + "cpuUsage": 0, + "memoryUsage": 0, + "scope": null, + "fex": false, + "health": -1, + "npvEnabled": false, + "linkName": null, + "username": null, + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": false, + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": false, + "switchRole": "tor", + "mode": "Normal", + "hostName": "tor", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": null, + "operMode": null, + "modules": null, + "fexMap": {}, + "isVpcConfigured": false, + "vpcDomain": 0, + "role": null, + "peer": null, + "peerSerialNumber": null, + "peerSwitchDbId": 0, + "peerlinkState": null, + "keepAliveState": null, + "consistencyState": false, + "sendIntf": null, + "recvIntf": null, + "interfaces": null, + "elementType": null, + "monitorMode": null, + "freezeMode": null, + "cfsSyslogStatus": 1, + "isNonNexus": false, + "swUUIDId": 99910, + "swUUID": "DCNM-UUID-99910", + "swType": null, + "ccStatus": "In-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + } + }, + + "contracts_fab_details": { + "id": 6, + "fabricId": "FABRIC-6", + "fabricName": "test_netv2", + "fabricType": "Switch_Fabric", + "fabricTypeFriendly": "Switch Fabric", + "fabricTechnology": "VXLANFabric", + "fabricTechnologyFriendly": "VXLAN EVPN", + "provisionMode": "DCNMTopDown", + "deviceType": "n9k", + "replicationMode": "Multicast", + "operStatus": "MINOR", + "asn": "32123", + "siteId": "32123", + "templateName": "Easy_Fabric", + "nvPairs": { + "MSO_SITE_ID": "", + "PHANTOM_RP_LB_ID1": "", + "PHANTOM_RP_LB_ID2": "", + "PHANTOM_RP_LB_ID3": "", + "IBGP_PEER_TEMPLATE": "", + "PHANTOM_RP_LB_ID4": "", + "abstract_ospf": "base_ospf", + "FEATURE_PTP": "false", + "L3_PARTITION_ID_RANGE": "50000-59000", + "DHCP_START_INTERNAL": "", + "SSPINE_COUNT": "0", + "NXC_DEST_VRF": "management", + "ADVERTISE_PIP_BGP": "false", + "FABRIC_VPC_QOS_POLICY_NAME": "spine_qos_for_fabric_vpc_peering", + "BFD_PIM_ENABLE": "false", + "DHCP_END": "", + "UNDERLAY_IS_V6": "false", + "FABRIC_VPC_DOMAIN_ID": "", + "SEED_SWITCH_CORE_INTERFACES": "", + "ALLOW_NXC_PREV": "true", + "FABRIC_MTU_PREV": "9216", + "BFD_ISIS_ENABLE": "false", + "HD_TIME": "180", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX": "false", + "OSPF_AUTH_ENABLE": "false", + "LOOPBACK1_IPV6_RANGE": "", + "ROUTER_ID_RANGE": "", + "MSO_CONNECTIVITY_DEPLOYED": "", + "ENABLE_MACSEC": "false", + "DEAFULT_QUEUING_POLICY_OTHER": "queuing_policy_default_other", + "UNNUM_DHCP_START_INTERNAL": "", + "MACSEC_REPORT_TIMER": "", + "PREMSO_PARENT_FABRIC": "", + "UNNUM_DHCP_END_INTERNAL": "", + "PTP_DOMAIN_ID": "", + "USE_LINK_LOCAL": "false", + "AUTO_SYMMETRIC_VRF_LITE": "false", + "BGP_AS_PREV": "32123", + "ENABLE_PBR": "false", + "DCI_SUBNET_TARGET_MASK": "30", + "VPC_PEER_LINK_PO": "500", + "ISIS_AUTH_ENABLE": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", + "REPLICATION_MODE": "Multicast", + "ANYCAST_RP_IP_RANGE": "110.254.254.0/24", + "VPC_ENABLE_IPv6_ND_SYNC": "true", + "abstract_isis_interface": "isis_interface", + "TCAM_ALLOCATION": "true", + "SERVICE_NETWORK_VLAN_RANGE": "3000-3199", + "MACSEC_ALGORITHM": "", + "ISIS_LEVEL": "level-2", + "SUBNET_TARGET_MASK": "30", + "abstract_anycast_rp": "anycast_rp", + "AUTO_SYMMETRIC_DEFAULT_VRF": "false", + "ENABLE_NETFLOW": "false", + "DEAFULT_QUEUING_POLICY_R_SERIES": "queuing_policy_default_r_series", + "temp_vpc_peer_link": "int_vpc_peer_link_po", + "BROWNFIELD_NETWORK_NAME_FORMAT": "Auto_Net_VNI$$VNI$$_VLAN$$VLAN_ID$$", + "ENABLE_FABRIC_VPC_DOMAIN_ID": "false", + "IBGP_PEER_TEMPLATE_LEAF": "", + "DCI_SUBNET_RANGE": "110.33.0.0/16", + "MGMT_GW_INTERNAL": "", + "ENABLE_NXAPI": "true", + "VRF_LITE_AUTOCONFIG": "Manual", + "GRFIELD_DEBUG_FLAG": "Disable", + "VRF_VLAN_RANGE": "2000-2299", + "ISIS_AUTH_KEYCHAIN_NAME": "", + "OBJECT_TRACKING_NUMBER_RANGE": "100-299", + "SSPINE_ADD_DEL_DEBUG_FLAG": "Disable", + "abstract_bgp_neighbor": "evpn_bgp_rr_neighbor", + "OSPF_AUTH_KEY_ID": "", + "PIM_HELLO_AUTH_ENABLE": "false", + "abstract_feature_leaf": "base_feature_leaf_upg", + "BFD_AUTH_ENABLE": "false", + "BGP_LB_ID": "0", + "LOOPBACK1_IP_RANGE": "10.31.0.0/22", + "EXTRA_CONF_TOR": "", + "AAA_SERVER_CONF": "", + "VPC_PEER_KEEP_ALIVE_OPTION": "management", + "AUTO_VRFLITE_IFC_DEFAULT_VRF": "false", + "enableRealTimeBackup": "", + "V6_SUBNET_TARGET_MASK": "126", + "STRICT_CC_MODE": "false", + "BROWNFIELD_SKIP_OVERLAY_NETWORK_ATTACHMENTS": "false", + "VPC_PEER_LINK_VLAN": "3600", + "abstract_trunk_host": "int_trunk_host", + "NXAPI_HTTP_PORT": "80", + "BGP_AUTH_ENABLE": "false", + "MST_INSTANCE_RANGE": "", + "PM_ENABLE_PREV": "false", + "NXC_PROXY_PORT": "8080", + "RP_MODE": "asm", + "enableScheduledBackup": "", + "abstract_ospf_interface": "ospf_interface_11_1", + "BFD_OSPF_ENABLE": "false", + "MACSEC_FALLBACK_ALGORITHM": "", + "UNNUM_DHCP_END": "", + "LOOPBACK0_IP_RANGE": "10.21.0.0/22", + "ENABLE_AAA": "false", + "DEPLOYMENT_FREEZE": "false", + "L2_HOST_INTF_MTU_PREV": "9216", + "NETFLOW_MONITOR_LIST": "", + "ENABLE_AGENT": "false", + "NTP_SERVER_IP_LIST": "", + "OVERLAY_MODE": "cli", + "MACSEC_FALLBACK_KEY_STRING": "", + "PER_VRF_LOOPBACK_AUTO_PROVISION_PREV": "false", + "FF": "Easy_Fabric", + "STP_ROOT_OPTION": "unmanaged", + "FABRIC_TYPE": "Switch_Fabric", + "ISIS_OVERLOAD_ENABLE": "false", + "NETFLOW_RECORD_LIST": "", + "SPINE_COUNT": "0", + "abstract_extra_config_bootstrap": "extra_config_bootstrap_11_1", + "MPLS_LOOPBACK_IP_RANGE": "", + "LINK_STATE_ROUTING_TAG_PREV": "UNDERLAY", + "DHCP_ENABLE": "false", + "BFD_AUTH_KEY_ID": "", + "MSO_SITE_GROUP_NAME": "", + "MGMT_PREFIX_INTERNAL": "", + "DHCP_IPV6_ENABLE_INTERNAL": "", + "BGP_AUTH_KEY_TYPE": "3", + "SITE_ID": "32123", + "temp_anycast_gateway": "anycast_gateway", + "BRFIELD_DEBUG_FLAG": "Disable", + "BGP_AS": "32123", + "BOOTSTRAP_MULTISUBNET": "#Scope_Start_IP, Scope_End_IP, Scope_Default_Gateway, Scope_Subnet_Prefix", + "ISIS_P2P_ENABLE": "false", + "ENABLE_NGOAM": "true", + "CDP_ENABLE": "false", + "PTP_LB_ID": "", + "DHCP_IPV6_ENABLE": "", + "MACSEC_KEY_STRING": "", + "TOPDOWN_CONFIG_RM_TRACKING": "notstarted", + "OSPF_AUTH_KEY": "", + "ENABLE_FABRIC_VPC_DOMAIN_ID_PREV": "false", + "EXTRA_CONF_LEAF": "", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "DHCP_START": "", + "ENABLE_TRM": "false", + "ENABLE_PVLAN_PREV": "false", + "FEATURE_PTP_INTERNAL": "false", + "ENABLE_NXAPI_HTTP": "true", + "abstract_isis": "base_isis_level2", + "MPLS_LB_ID": "", + "FABRIC_VPC_DOMAIN_ID_PREV": "", + "ROUTE_MAP_SEQUENCE_NUMBER_RANGE": "1-65534", + "NETWORK_VLAN_RANGE": "2300-2999", + "STATIC_UNDERLAY_IP_ALLOC": "false", + "MGMT_V6PREFIX_INTERNAL": "", + "MPLS_HANDOFF": "false", + "STP_BRIDGE_PRIORITY": "", + "scheduledTime": "", + "ANYCAST_LB_ID": "", + "MACSEC_CIPHER_SUITE": "", + "STP_VLAN_RANGE": "", + "MSO_CONTROLER_ID": "", + "POWER_REDUNDANCY_MODE": "ps-redundant", + "BFD_ENABLE": "false", + "abstract_extra_config_leaf": "extra_config_leaf", + "ANYCAST_GW_MAC": "2020.0000.00aa", + "abstract_dhcp": "base_dhcp", + "default_pvlan_sec_network": "", + "EXTRA_CONF_SPINE": "", + "NTP_SERVER_VRF": "", + "SPINE_SWITCH_CORE_INTERFACES": "", + "LINK_STATE_ROUTING_TAG": "UNDERLAY", + "ISIS_OVERLOAD_ELAPSE_TIME": "", + "RP_LB_ID": "254", + "BOOTSTRAP_CONF": "", + "LINK_STATE_ROUTING": "ospf", + "ISIS_AUTH_KEY": "", + "network_extension_template": "Default_Network_Extension_Universal", + "DNS_SERVER_IP_LIST": "", + "DOMAIN_NAME_INTERNAL": "", + "ENABLE_EVPN": "true", + "abstract_multicast": "base_multicast_11_1", + "VPC_DELAY_RESTORE_TIME": "60", + "BFD_AUTH_KEY": "", + "ESR_OPTION": "PBR", + "AGENT_INTF": "eth0", + "FABRIC_MTU": "9216", + "L3VNI_MCAST_GROUP": "", + "UNNUM_BOOTSTRAP_LB_ID": "", + "VPC_DOMAIN_ID_RANGE": "1-1000", + "HOST_INTF_ADMIN_STATE": "true", + "BFD_IBGP_ENABLE": "false", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX_PREV": "false", + "VPC_AUTO_RECOVERY_TIME": "360", + "DNS_SERVER_VRF": "", + "UPGRADE_FROM_VERSION": "", + "BANNER": "", + "NXC_SRC_INTF": "", + "PER_VRF_LOOPBACK_IP_RANGE": "", + "SYSLOG_SEV": "", + "abstract_loopback_interface": "int_fabric_loopback_11_1", + "SYSLOG_SERVER_VRF": "", + "EXTRA_CONF_INTRA_LINKS": "", + "SNMP_SERVER_HOST_TRAP": "true", + "abstract_extra_config_spine": "extra_config_spine", + "PIM_HELLO_AUTH_KEY": "", + "temp_vpc_domain_mgmt": "vpc_domain_mgmt", + "V6_SUBNET_RANGE": "", + "SUBINTERFACE_RANGE": "2-511", + "abstract_routed_host": "int_routed_host", + "BGP_AUTH_KEY": "", + "ENABLE_PVLAN": "false", + "INBAND_DHCP_SERVERS": "", + "default_network": "Default_Network_Universal", + "ISIS_AUTH_KEYCHAIN_KEY_ID": "", + "MGMT_V6PREFIX": "", + "abstract_feature_spine": "base_feature_spine_upg", + "ENABLE_DEFAULT_QUEUING_POLICY": "false", + "ANYCAST_BGW_ADVERTISE_PIP": "false", + "NETFLOW_EXPORTER_LIST": "", + "abstract_vlan_interface": "int_fabric_vlan_11_1", + "RP_COUNT": "2", + "FABRIC_NAME": "test_netv2", + "abstract_pim_interface": "pim_interface", + "PM_ENABLE": "false", + "LOOPBACK0_IPV6_RANGE": "", + "dcnmUser": "admin", + "DEFAULT_VRF_REDIS_BGP_RMAP": "", + "NVE_LB_ID": "1", + "OVERLAY_MODE_PREV": "cli", + "VPC_DELAY_RESTORE": "150", + "NXAPI_HTTPS_PORT": "443", + "ENABLE_VPC_PEER_LINK_NATIVE_VLAN": "false", + "L2_HOST_INTF_MTU": "9216", + "abstract_route_map": "route_map", + "INBAND_MGMT_PREV": "false", + "EXT_FABRIC_TYPE": "", + "abstract_vpc_domain": "base_vpc_domain_11_1", + "ACTIVE_MIGRATION": "false", + "COPP_POLICY": "strict", + "DHCP_END_INTERNAL": "", + "BOOTSTRAP_ENABLE": "false", + "ADVERTISE_PIP_ON_BORDER": "true", + "default_vrf": "Default_VRF_Universal", + "NXC_PROXY_SERVER": "", + "OSPF_AREA_ID": "0.0.0.0", + "abstract_extra_config_tor": "extra_config_tor", + "SYSLOG_SERVER_IP_LIST": "", + "BOOTSTRAP_ENABLE_PREV": "false", + "ENABLE_TENANT_DHCP": "true", + "ANYCAST_RP_IP_RANGE_INTERNAL": "110.254.254.0/24", + "RR_COUNT": "2", + "BOOTSTRAP_MULTISUBNET_INTERNAL": "", + "MGMT_GW": "", + "UNNUM_DHCP_START": "", + "MGMT_PREFIX": "", + "abstract_bgp_rr": "evpn_bgp_rr", + "INBAND_MGMT": "false", + "abstract_bgp": "base_bgp", + "SLA_ID_RANGE": "10000-19999", + "ENABLE_NETFLOW_PREV": "false", + "SUBNET_RANGE": "110.4.0.0/16", + "DEAFULT_QUEUING_POLICY_CLOUDSCALE": "queuing_policy_default_8q_cloudscale", + "MULTICAST_GROUP_SUBNET": "239.11.11.0/25", + "FABRIC_INTERFACE_TYPE": "p2p", + "ALLOW_NXC": "true", + "OVERWRITE_GLOBAL_NXC": "false", + "FABRIC_VPC_QOS": "false", + "AAA_REMOTE_IP_ENABLED": "false", + "L2_SEGMENT_ID_RANGE": "30000-49000" + }, + "vrfTemplate": "Default_VRF_Universal", + "networkTemplate": "Default_Network_Universal", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "modifiedOn": 1713867710252 + }, + + "dcnm_contracts_empty_resp": { + "RETURN_CODE":200, + "METHOD":"GET", + "REQUEST_PATH":"https://192.168.2.1:443/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/contracts", + "MESSAGE":"OK", + "DATA":[] + }, + + "dcnm_contracts_create_resp": { + "RETURN_CODE":200, + "METHOD":"POST", + "REQUEST_PATH":"https://192.168.2.1:443/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/contracts", + "MESSAGE":"OK", + "DATA":{ + "code": "200", + "failedCount": 0, + "failureList": [], + "message": "success", + "successCount": 1, + "successList": [ + { + "code": "201", + "location": "/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/contracts/test", + "message": "test create successful", + "name": "test", + "resourceId": "{\"contractName\":\"test\"}", + "status": "success", + "uuid": "68a3b92c-25ed-4fc1-add4-192913daafe4" + } + ], + "totalCount": 1 + } + }, + + "dcnm_contracts_modify_resp": { + "RETURN_CODE":200, + "METHOD":"PUT", + "REQUEST_PATH":"https://192.168.2.1:443/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/contracts", + "MESSAGE":"OK", + "DATA":{ + "code": "200", + "failedCount": 0, + "failureList": [], + "message": "success", + "successCount": 1, + "successList": [ + { + "code": "201", + "location": "/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/contracts/test", + "message": "test create successful", + "name": "test", + "resourceId": "{\"contractName\":\"test\"}", + "status": "success", + "uuid": "68a3b92c-25ed-4fc1-add4-192913daafe4" + } + ], + "totalCount": 1 + } + }, + + "dcnm_contracts_get_resp": { + "RETURN_CODE":200, + "METHOD":"GET", + "REQUEST_PATH":"https://192.168.2.1:443/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/contracts", + "MESSAGE":"OK", + "DATA":[ + { + "fabricName":"test_fab", + "contractName":"test1", + "description":"test1", + "rules":[ + { + "direction":"bidirectional", + "action":"deny", + "protocolName":"test1", + "uuid":"1c2b0265-e4a7-4b39-bb31-a274cf9e6631", + "matchSummary":"IPv4 TCP" + } + ] + }, + { + "fabricName":"test_fab", + "contractName":"test", + "description":"test", + "rules":[ + { + "direction":"bidirectional", + "action":"permit", + "protocolName":"http", + "uuid":"a88f0433-f250-45fe-a717-94b3a4ccf8fa", + "matchSummary":"IP HTTP" + } + ] + } + ] + }, + + "dcnm_contracts_get_one_resp": { + "RETURN_CODE":200, + "METHOD":"GET", + "REQUEST_PATH":"https://192.168.2.1:443/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/contracts/test", + "MESSAGE":"OK", + "DATA": + { + "fabricName":"test_fab", + "contractName":"test", + "description":"test", + "rules":[ + { + "direction":"bidirectional", + "action":"permit", + "protocolName":"http", + "uuid":"a88f0433-f250-45fe-a717-94b3a4ccf8fa", + "matchSummary":"" + } + ] + } + }, + + "dcnm_contracts_conf_delete_resp": { + "DATA": { + "code": "200", + "failedCount": 0, + "failureList": [], + "message": "success", + "successCount": 2, + "successList": [ + { + "code": "200", + "message": "test delete successful", + "name": "test", + "resourceId": "{\"contractName\":\"test\"}", + "status": "deleted" + } + ], + "totalCount": 2 + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://192.168.2.1:443/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/contracts/bulkDelete", + "RETURN_CODE": 200 + }, + + "dcnm_contracts_delete_resp": { + "DATA": { + "code": "200", + "failedCount": 0, + "failureList": [], + "message": "success", + "successCount": 2, + "successList": [ + { + "code": "200", + "message": "test delete successful", + "name": "test", + "resourceId": "{\"contractName\":\"test\"}", + "status": "deleted" + }, + { + "code": "200", + "message": "http delete successful", + "name": "http", + "resourceId": "{\"contractName\":\"http\"}", + "status": "deleted" + } + ], + "totalCount": 2 + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://10.78.210.227:443/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/contracts/bulkDelete", + "RETURN_CODE": 200 + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_protocols/dcnm_protocols_common.py b/tests/unit/modules/dcnm/fixtures/dcnm_protocols/dcnm_protocols_common.py new file mode 100644 index 000000000..833dcd819 --- /dev/null +++ b/tests/unit/modules/dcnm/fixtures/dcnm_protocols/dcnm_protocols_common.py @@ -0,0 +1,55 @@ +# 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_protocols import ( + DcnmProtocols, +) + + +class MockAnsibleModule: + """ + Mock the AnsibleModule class + """ + + params = { + "config": [], + "state": "merged", + "fabric": "test_fab", + } + supports_check_mode = True + + @staticmethod + def fail_json(msg, **kwargs) -> AnsibleFailJson: + """ + mock the fail_json method + """ + raise AnsibleFailJson(msg, kwargs) + + +@pytest.fixture(name="dcnm_protocols_fixture") +def dcnm_protocols_fixture(monkeypatch): + """ + mock DcnmProtocols + """ + + return DcnmProtocols(MockAnsibleModule) diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_protocols/dcnm_protocols_data.json b/tests/unit/modules/dcnm/fixtures/dcnm_protocols/dcnm_protocols_data.json new file mode 100644 index 000000000..0591f1afd --- /dev/null +++ b/tests/unit/modules/dcnm/fixtures/dcnm_protocols/dcnm_protocols_data.json @@ -0,0 +1,45 @@ +{ + "protocols_cfg_00001": + { + "protocol_name": "test", + "description": "test", + "match": [ + { + "type": "ip", + "protocol_options": "tcp", + "source_port_range": 10 + } + ] + }, + + "protocols_cfg_00002": + { + "protocol_name": "test", + "description": "test", + "match": [ + { + "type": "ip", + "protocol_options": "tcp", + "source_port_range": 10 + }, + { + "type": "ipv4", + "protocol_options": "icmp", + "dscp": 10 + } + ] + }, + + "protocols_cfg_00003": + { + "protocol_name": "test", + "description": "test", + "match": [ + { + "type": "ipv6", + "protocol_options": "tcp", + "source_port_range": 10 + } + ] + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_protocols/dcnm_protocols_response.json b/tests/unit/modules/dcnm/fixtures/dcnm_protocols/dcnm_protocols_response.json new file mode 100644 index 000000000..a2c0d206c --- /dev/null +++ b/tests/unit/modules/dcnm/fixtures/dcnm_protocols/dcnm_protocols_response.json @@ -0,0 +1,808 @@ +{ + "protocols_inv_details": { + "192.168.2.1": { + "switchRoleEnum": "Leaf", + "vrf": "management", + "fabricTechnology": "VXLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 6, + "name": null, + "domainID": 0, + "wwn": null, + "membership": null, + "ports": 0, + "model": "N9K-C9300v", + "version": null, + "upTime": 0, + "ipAddress": "192.168.2.1", + "mgmtAddress": null, + "vendor": "Cisco", + "displayHdrs": null, + "displayValues": null, + "colDBId": 0, + "fid": 0, + "isLan": false, + "is_smlic_enabled": false, + "present": true, + "licenseViolation": false, + "managable": true, + "mds": false, + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "test_netv2", + "modelType": 0, + "logicalName": "leaf1", + "switchDbID": 86340, + "uid": 0, + "release": "10.3(1)", + "location": null, + "contact": null, + "upTimeStr": "21 days, 01:17:37", + "upTimeNumber": 0, + "network": null, + "nonMdsModel": null, + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": null, + "vsanWwnName": null, + "swWwn": null, + "swWwnName": null, + "serialNumber": "9SFRKD0M6AS", + "domain": null, + "principal": null, + "status": "ok", + "index": 0, + "licenseDetail": null, + "isPmCollect": false, + "sanAnalyticsCapable": false, + "vdcId": 0, + "vdcName": "", + "vdcMac": null, + "fcoeEnabled": false, + "cpuUsage": 0, + "memoryUsage": 0, + "scope": null, + "fex": false, + "health": -1, + "npvEnabled": false, + "linkName": null, + "username": null, + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": false, + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": false, + "switchRole": "leaf", + "mode": "Normal", + "hostName": "leaf1", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": null, + "operMode": null, + "modules": null, + "fexMap": {}, + "isVpcConfigured": false, + "vpcDomain": 0, + "role": null, + "peer": null, + "peerSerialNumber": null, + "peerSwitchDbId": 0, + "peerlinkState": null, + "keepAliveState": null, + "consistencyState": false, + "sendIntf": null, + "recvIntf": null, + "interfaces": null, + "elementType": null, + "monitorMode": null, + "freezeMode": null, + "cfsSyslogStatus": 1, + "isNonNexus": false, + "swUUIDId": 86370, + "swUUID": "DCNM-UUID-86370", + "swType": null, + "ccStatus": "In-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + }, + "192.168.2.2": { + "switchRoleEnum": "Leaf", + "vrf": "management", + "fabricTechnology": "VXLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 6, + "name": null, + "domainID": 0, + "wwn": null, + "membership": null, + "ports": 0, + "model": "N9K-C9300v", + "version": null, + "upTime": 0, + "ipAddress": "192.168.2.2", + "mgmtAddress": null, + "vendor": "Cisco", + "displayHdrs": null, + "displayValues": null, + "colDBId": 0, + "fid": 0, + "isLan": false, + "is_smlic_enabled": false, + "present": true, + "licenseViolation": false, + "managable": true, + "mds": false, + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "test_netv2", + "modelType": 0, + "logicalName": "leaf2", + "switchDbID": 86290, + "uid": 0, + "release": "10.3(1)", + "location": null, + "contact": null, + "upTimeStr": "21 days, 01:17:32", + "upTimeNumber": 0, + "network": null, + "nonMdsModel": null, + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": null, + "vsanWwnName": null, + "swWwn": null, + "swWwnName": null, + "serialNumber": "9KRDG57QQZT", + "domain": null, + "principal": null, + "status": "ok", + "index": 0, + "licenseDetail": null, + "isPmCollect": false, + "sanAnalyticsCapable": false, + "vdcId": 0, + "vdcName": "", + "vdcMac": null, + "fcoeEnabled": false, + "cpuUsage": 0, + "memoryUsage": 0, + "scope": null, + "fex": false, + "health": -1, + "npvEnabled": false, + "linkName": null, + "username": null, + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": false, + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": false, + "switchRole": "leaf", + "mode": "Normal", + "hostName": "leaf2", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": null, + "operMode": null, + "modules": null, + "fexMap": {}, + "isVpcConfigured": false, + "vpcDomain": 0, + "role": null, + "peer": null, + "peerSerialNumber": null, + "peerSwitchDbId": 0, + "peerlinkState": null, + "keepAliveState": null, + "consistencyState": false, + "sendIntf": null, + "recvIntf": null, + "interfaces": null, + "elementType": null, + "monitorMode": null, + "freezeMode": null, + "cfsSyslogStatus": 1, + "isNonNexus": false, + "swUUIDId": 86320, + "swUUID": "DCNM-UUID-86320", + "swType": null, + "ccStatus": "In-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + }, + "192.168.2.3": { + "switchRoleEnum": "Tor", + "vrf": "management", + "fabricTechnology": "VXLANFabric", + "deviceType": "Switch_Fabric", + "fabricId": 6, + "name": null, + "domainID": 0, + "wwn": null, + "membership": null, + "ports": 0, + "model": "N9K-C9300v", + "version": null, + "upTime": 0, + "ipAddress": "192.168.2.3", + "mgmtAddress": null, + "vendor": "Cisco", + "displayHdrs": null, + "displayValues": null, + "colDBId": 0, + "fid": 0, + "isLan": false, + "is_smlic_enabled": false, + "present": true, + "licenseViolation": false, + "managable": true, + "mds": false, + "connUnitStatus": 0, + "standbySupState": 0, + "activeSupSlot": 0, + "unmanagableCause": "", + "lastScanTime": 0, + "fabricName": "test_netv2", + "modelType": 0, + "logicalName": "tor", + "switchDbID": 101110, + "uid": 0, + "release": "10.3(1)", + "location": null, + "contact": null, + "upTimeStr": "21 days, 00:27:44", + "upTimeNumber": 0, + "network": null, + "nonMdsModel": null, + "numberOfPorts": 0, + "availPorts": 0, + "usedPorts": 0, + "vsanWwn": null, + "vsanWwnName": null, + "swWwn": null, + "swWwnName": null, + "serialNumber": "959A4D0NYXI", + "domain": null, + "principal": null, + "status": "ok", + "index": 0, + "licenseDetail": null, + "isPmCollect": false, + "sanAnalyticsCapable": false, + "vdcId": 0, + "vdcName": "", + "vdcMac": null, + "fcoeEnabled": false, + "cpuUsage": 0, + "memoryUsage": 0, + "scope": null, + "fex": false, + "health": -1, + "npvEnabled": false, + "linkName": null, + "username": null, + "primaryIP": "", + "primarySwitchDbID": 0, + "secondaryIP": "", + "secondarySwitchDbID": 0, + "isEchSupport": false, + "moduleIndexOffset": 9999, + "sysDescr": "", + "isTrapDelayed": false, + "switchRole": "tor", + "mode": "Normal", + "hostName": "tor", + "ipDomain": "", + "systemMode": "Normal", + "sourceVrf": "management", + "sourceInterface": "mgmt0", + "protoDiscSettings": null, + "operMode": null, + "modules": null, + "fexMap": {}, + "isVpcConfigured": false, + "vpcDomain": 0, + "role": null, + "peer": null, + "peerSerialNumber": null, + "peerSwitchDbId": 0, + "peerlinkState": null, + "keepAliveState": null, + "consistencyState": false, + "sendIntf": null, + "recvIntf": null, + "interfaces": null, + "elementType": null, + "monitorMode": null, + "freezeMode": null, + "cfsSyslogStatus": 1, + "isNonNexus": false, + "swUUIDId": 99910, + "swUUID": "DCNM-UUID-99910", + "swType": null, + "ccStatus": "In-Sync", + "operStatus": "Minor", + "intentedpeerName": "" + } + }, + + "protocols_fab_details": { + "id": 6, + "fabricId": "FABRIC-6", + "fabricName": "test_netv2", + "fabricType": "Switch_Fabric", + "fabricTypeFriendly": "Switch Fabric", + "fabricTechnology": "VXLANFabric", + "fabricTechnologyFriendly": "VXLAN EVPN", + "provisionMode": "DCNMTopDown", + "deviceType": "n9k", + "replicationMode": "Multicast", + "operStatus": "MINOR", + "asn": "32123", + "siteId": "32123", + "templateName": "Easy_Fabric", + "nvPairs": { + "MSO_SITE_ID": "", + "PHANTOM_RP_LB_ID1": "", + "PHANTOM_RP_LB_ID2": "", + "PHANTOM_RP_LB_ID3": "", + "IBGP_PEER_TEMPLATE": "", + "PHANTOM_RP_LB_ID4": "", + "abstract_ospf": "base_ospf", + "FEATURE_PTP": "false", + "L3_PARTITION_ID_RANGE": "50000-59000", + "DHCP_START_INTERNAL": "", + "SSPINE_COUNT": "0", + "NXC_DEST_VRF": "management", + "ADVERTISE_PIP_BGP": "false", + "FABRIC_VPC_QOS_POLICY_NAME": "spine_qos_for_fabric_vpc_peering", + "BFD_PIM_ENABLE": "false", + "DHCP_END": "", + "UNDERLAY_IS_V6": "false", + "FABRIC_VPC_DOMAIN_ID": "", + "SEED_SWITCH_CORE_INTERFACES": "", + "ALLOW_NXC_PREV": "true", + "FABRIC_MTU_PREV": "9216", + "BFD_ISIS_ENABLE": "false", + "HD_TIME": "180", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX": "false", + "OSPF_AUTH_ENABLE": "false", + "LOOPBACK1_IPV6_RANGE": "", + "ROUTER_ID_RANGE": "", + "MSO_CONNECTIVITY_DEPLOYED": "", + "ENABLE_MACSEC": "false", + "DEAFULT_QUEUING_POLICY_OTHER": "queuing_policy_default_other", + "UNNUM_DHCP_START_INTERNAL": "", + "MACSEC_REPORT_TIMER": "", + "PREMSO_PARENT_FABRIC": "", + "UNNUM_DHCP_END_INTERNAL": "", + "PTP_DOMAIN_ID": "", + "USE_LINK_LOCAL": "false", + "AUTO_SYMMETRIC_VRF_LITE": "false", + "BGP_AS_PREV": "32123", + "ENABLE_PBR": "false", + "DCI_SUBNET_TARGET_MASK": "30", + "VPC_PEER_LINK_PO": "500", + "ISIS_AUTH_ENABLE": "false", + "PER_VRF_LOOPBACK_AUTO_PROVISION": "false", + "REPLICATION_MODE": "Multicast", + "ANYCAST_RP_IP_RANGE": "110.254.254.0/24", + "VPC_ENABLE_IPv6_ND_SYNC": "true", + "abstract_isis_interface": "isis_interface", + "TCAM_ALLOCATION": "true", + "SERVICE_NETWORK_VLAN_RANGE": "3000-3199", + "MACSEC_ALGORITHM": "", + "ISIS_LEVEL": "level-2", + "SUBNET_TARGET_MASK": "30", + "abstract_anycast_rp": "anycast_rp", + "AUTO_SYMMETRIC_DEFAULT_VRF": "false", + "ENABLE_NETFLOW": "false", + "DEAFULT_QUEUING_POLICY_R_SERIES": "queuing_policy_default_r_series", + "temp_vpc_peer_link": "int_vpc_peer_link_po", + "BROWNFIELD_NETWORK_NAME_FORMAT": "Auto_Net_VNI$$VNI$$_VLAN$$VLAN_ID$$", + "ENABLE_FABRIC_VPC_DOMAIN_ID": "false", + "IBGP_PEER_TEMPLATE_LEAF": "", + "DCI_SUBNET_RANGE": "110.33.0.0/16", + "MGMT_GW_INTERNAL": "", + "ENABLE_NXAPI": "true", + "VRF_LITE_AUTOCONFIG": "Manual", + "GRFIELD_DEBUG_FLAG": "Disable", + "VRF_VLAN_RANGE": "2000-2299", + "ISIS_AUTH_KEYCHAIN_NAME": "", + "OBJECT_TRACKING_NUMBER_RANGE": "100-299", + "SSPINE_ADD_DEL_DEBUG_FLAG": "Disable", + "abstract_bgp_neighbor": "evpn_bgp_rr_neighbor", + "OSPF_AUTH_KEY_ID": "", + "PIM_HELLO_AUTH_ENABLE": "false", + "abstract_feature_leaf": "base_feature_leaf_upg", + "BFD_AUTH_ENABLE": "false", + "BGP_LB_ID": "0", + "LOOPBACK1_IP_RANGE": "10.31.0.0/22", + "EXTRA_CONF_TOR": "", + "AAA_SERVER_CONF": "", + "VPC_PEER_KEEP_ALIVE_OPTION": "management", + "AUTO_VRFLITE_IFC_DEFAULT_VRF": "false", + "enableRealTimeBackup": "", + "V6_SUBNET_TARGET_MASK": "126", + "STRICT_CC_MODE": "false", + "BROWNFIELD_SKIP_OVERLAY_NETWORK_ATTACHMENTS": "false", + "VPC_PEER_LINK_VLAN": "3600", + "abstract_trunk_host": "int_trunk_host", + "NXAPI_HTTP_PORT": "80", + "BGP_AUTH_ENABLE": "false", + "MST_INSTANCE_RANGE": "", + "PM_ENABLE_PREV": "false", + "NXC_PROXY_PORT": "8080", + "RP_MODE": "asm", + "enableScheduledBackup": "", + "abstract_ospf_interface": "ospf_interface_11_1", + "BFD_OSPF_ENABLE": "false", + "MACSEC_FALLBACK_ALGORITHM": "", + "UNNUM_DHCP_END": "", + "LOOPBACK0_IP_RANGE": "10.21.0.0/22", + "ENABLE_AAA": "false", + "DEPLOYMENT_FREEZE": "false", + "L2_HOST_INTF_MTU_PREV": "9216", + "NETFLOW_MONITOR_LIST": "", + "ENABLE_AGENT": "false", + "NTP_SERVER_IP_LIST": "", + "OVERLAY_MODE": "cli", + "MACSEC_FALLBACK_KEY_STRING": "", + "PER_VRF_LOOPBACK_AUTO_PROVISION_PREV": "false", + "FF": "Easy_Fabric", + "STP_ROOT_OPTION": "unmanaged", + "FABRIC_TYPE": "Switch_Fabric", + "ISIS_OVERLOAD_ENABLE": "false", + "NETFLOW_RECORD_LIST": "", + "SPINE_COUNT": "0", + "abstract_extra_config_bootstrap": "extra_config_bootstrap_11_1", + "MPLS_LOOPBACK_IP_RANGE": "", + "LINK_STATE_ROUTING_TAG_PREV": "UNDERLAY", + "DHCP_ENABLE": "false", + "BFD_AUTH_KEY_ID": "", + "MSO_SITE_GROUP_NAME": "", + "MGMT_PREFIX_INTERNAL": "", + "DHCP_IPV6_ENABLE_INTERNAL": "", + "BGP_AUTH_KEY_TYPE": "3", + "SITE_ID": "32123", + "temp_anycast_gateway": "anycast_gateway", + "BRFIELD_DEBUG_FLAG": "Disable", + "BGP_AS": "32123", + "BOOTSTRAP_MULTISUBNET": "#Scope_Start_IP, Scope_End_IP, Scope_Default_Gateway, Scope_Subnet_Prefix", + "ISIS_P2P_ENABLE": "false", + "ENABLE_NGOAM": "true", + "CDP_ENABLE": "false", + "PTP_LB_ID": "", + "DHCP_IPV6_ENABLE": "", + "MACSEC_KEY_STRING": "", + "TOPDOWN_CONFIG_RM_TRACKING": "notstarted", + "OSPF_AUTH_KEY": "", + "ENABLE_FABRIC_VPC_DOMAIN_ID_PREV": "false", + "EXTRA_CONF_LEAF": "", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "DHCP_START": "", + "ENABLE_TRM": "false", + "ENABLE_PVLAN_PREV": "false", + "FEATURE_PTP_INTERNAL": "false", + "ENABLE_NXAPI_HTTP": "true", + "abstract_isis": "base_isis_level2", + "MPLS_LB_ID": "", + "FABRIC_VPC_DOMAIN_ID_PREV": "", + "ROUTE_MAP_SEQUENCE_NUMBER_RANGE": "1-65534", + "NETWORK_VLAN_RANGE": "2300-2999", + "STATIC_UNDERLAY_IP_ALLOC": "false", + "MGMT_V6PREFIX_INTERNAL": "", + "MPLS_HANDOFF": "false", + "STP_BRIDGE_PRIORITY": "", + "scheduledTime": "", + "ANYCAST_LB_ID": "", + "MACSEC_CIPHER_SUITE": "", + "STP_VLAN_RANGE": "", + "MSO_CONTROLER_ID": "", + "POWER_REDUNDANCY_MODE": "ps-redundant", + "BFD_ENABLE": "false", + "abstract_extra_config_leaf": "extra_config_leaf", + "ANYCAST_GW_MAC": "2020.0000.00aa", + "abstract_dhcp": "base_dhcp", + "default_pvlan_sec_network": "", + "EXTRA_CONF_SPINE": "", + "NTP_SERVER_VRF": "", + "SPINE_SWITCH_CORE_INTERFACES": "", + "LINK_STATE_ROUTING_TAG": "UNDERLAY", + "ISIS_OVERLOAD_ELAPSE_TIME": "", + "RP_LB_ID": "254", + "BOOTSTRAP_CONF": "", + "LINK_STATE_ROUTING": "ospf", + "ISIS_AUTH_KEY": "", + "network_extension_template": "Default_Network_Extension_Universal", + "DNS_SERVER_IP_LIST": "", + "DOMAIN_NAME_INTERNAL": "", + "ENABLE_EVPN": "true", + "abstract_multicast": "base_multicast_11_1", + "VPC_DELAY_RESTORE_TIME": "60", + "BFD_AUTH_KEY": "", + "ESR_OPTION": "PBR", + "AGENT_INTF": "eth0", + "FABRIC_MTU": "9216", + "L3VNI_MCAST_GROUP": "", + "UNNUM_BOOTSTRAP_LB_ID": "", + "VPC_DOMAIN_ID_RANGE": "1-1000", + "HOST_INTF_ADMIN_STATE": "true", + "BFD_IBGP_ENABLE": "false", + "AUTO_UNIQUE_VRF_LITE_IP_PREFIX_PREV": "false", + "VPC_AUTO_RECOVERY_TIME": "360", + "DNS_SERVER_VRF": "", + "UPGRADE_FROM_VERSION": "", + "BANNER": "", + "NXC_SRC_INTF": "", + "PER_VRF_LOOPBACK_IP_RANGE": "", + "SYSLOG_SEV": "", + "abstract_loopback_interface": "int_fabric_loopback_11_1", + "SYSLOG_SERVER_VRF": "", + "EXTRA_CONF_INTRA_LINKS": "", + "SNMP_SERVER_HOST_TRAP": "true", + "abstract_extra_config_spine": "extra_config_spine", + "PIM_HELLO_AUTH_KEY": "", + "temp_vpc_domain_mgmt": "vpc_domain_mgmt", + "V6_SUBNET_RANGE": "", + "SUBINTERFACE_RANGE": "2-511", + "abstract_routed_host": "int_routed_host", + "BGP_AUTH_KEY": "", + "ENABLE_PVLAN": "false", + "INBAND_DHCP_SERVERS": "", + "default_network": "Default_Network_Universal", + "ISIS_AUTH_KEYCHAIN_KEY_ID": "", + "MGMT_V6PREFIX": "", + "abstract_feature_spine": "base_feature_spine_upg", + "ENABLE_DEFAULT_QUEUING_POLICY": "false", + "ANYCAST_BGW_ADVERTISE_PIP": "false", + "NETFLOW_EXPORTER_LIST": "", + "abstract_vlan_interface": "int_fabric_vlan_11_1", + "RP_COUNT": "2", + "FABRIC_NAME": "test_netv2", + "abstract_pim_interface": "pim_interface", + "PM_ENABLE": "false", + "LOOPBACK0_IPV6_RANGE": "", + "dcnmUser": "admin", + "DEFAULT_VRF_REDIS_BGP_RMAP": "", + "NVE_LB_ID": "1", + "OVERLAY_MODE_PREV": "cli", + "VPC_DELAY_RESTORE": "150", + "NXAPI_HTTPS_PORT": "443", + "ENABLE_VPC_PEER_LINK_NATIVE_VLAN": "false", + "L2_HOST_INTF_MTU": "9216", + "abstract_route_map": "route_map", + "INBAND_MGMT_PREV": "false", + "EXT_FABRIC_TYPE": "", + "abstract_vpc_domain": "base_vpc_domain_11_1", + "ACTIVE_MIGRATION": "false", + "COPP_POLICY": "strict", + "DHCP_END_INTERNAL": "", + "BOOTSTRAP_ENABLE": "false", + "ADVERTISE_PIP_ON_BORDER": "true", + "default_vrf": "Default_VRF_Universal", + "NXC_PROXY_SERVER": "", + "OSPF_AREA_ID": "0.0.0.0", + "abstract_extra_config_tor": "extra_config_tor", + "SYSLOG_SERVER_IP_LIST": "", + "BOOTSTRAP_ENABLE_PREV": "false", + "ENABLE_TENANT_DHCP": "true", + "ANYCAST_RP_IP_RANGE_INTERNAL": "110.254.254.0/24", + "RR_COUNT": "2", + "BOOTSTRAP_MULTISUBNET_INTERNAL": "", + "MGMT_GW": "", + "UNNUM_DHCP_START": "", + "MGMT_PREFIX": "", + "abstract_bgp_rr": "evpn_bgp_rr", + "INBAND_MGMT": "false", + "abstract_bgp": "base_bgp", + "SLA_ID_RANGE": "10000-19999", + "ENABLE_NETFLOW_PREV": "false", + "SUBNET_RANGE": "110.4.0.0/16", + "DEAFULT_QUEUING_POLICY_CLOUDSCALE": "queuing_policy_default_8q_cloudscale", + "MULTICAST_GROUP_SUBNET": "239.11.11.0/25", + "FABRIC_INTERFACE_TYPE": "p2p", + "ALLOW_NXC": "true", + "OVERWRITE_GLOBAL_NXC": "false", + "FABRIC_VPC_QOS": "false", + "AAA_REMOTE_IP_ENABLED": "false", + "L2_SEGMENT_ID_RANGE": "30000-49000" + }, + "vrfTemplate": "Default_VRF_Universal", + "networkTemplate": "Default_Network_Universal", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "modifiedOn": 1713867710252 + }, + + "dcnm_protocols_empty_resp": { + "RETURN_CODE":200, + "METHOD":"GET", + "REQUEST_PATH":"https://192.168.2.1:443/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/protocols", + "MESSAGE":"OK", + "DATA":[] + }, + + + "dcnm_protocols_create_resp": { + "RETURN_CODE":200, + "METHOD":"POST", + "REQUEST_PATH":"https://192.168.2.1:443/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/protocols", + "MESSAGE":"OK", + "DATA":{ + "code": "200", + "failedCount": 0, + "failureList": [], + "message": "success", + "successCount": 1, + "successList": [ + { + "code": "201", + "location": "/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/protocols/test", + "message": "test create successful", + "name": "test", + "resourceId": "{\"protocolName\":\"test\"}", + "status": "success", + "uuid": "68a3b92c-25ed-4fc1-add4-192913daafe4" + } + ], + "totalCount": 1 + } + }, + + "dcnm_protocols_modify_resp": { + "RETURN_CODE":200, + "METHOD":"PUT", + "REQUEST_PATH":"https://192.168.2.1:443/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/protocols", + "MESSAGE":"OK", + "DATA":{ + "code": "200", + "failedCount": 0, + "failureList": [], + "message": "success", + "successCount": 1, + "successList": [ + { + "code": "201", + "location": "/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/protocols/test", + "message": "test create successful", + "name": "test", + "resourceId": "{\"protocolName\":\"test\"}", + "status": "success", + "uuid": "68a3b92c-25ed-4fc1-add4-192913daafe4" + } + ], + "totalCount": 1 + } + }, + + "dcnm_protocols_get_resp": { + "RETURN_CODE":200, + "METHOD":"GET", + "REQUEST_PATH":"https://192.168.2.1:443/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/protocols", + "MESSAGE":"OK", + "DATA":[ + { + "fabricName":"test_fab", + "protocolName":"test", + "matchType":"any", + "matchItems":[ + { + "type":"IP", + "protocolOptions":"UDP" + } + ], + "matchSummary":"IP UDP", + "associatedContractCount":1 + }, + { + "fabricName":"test_fab", + "protocolName":"test1", + "description":"test1", + "matchType":"any", + "matchItems":[ + { + "type":"IPv4", + "protocolOptions":"TCP" + } + ], + "matchSummary":"IPv4 TCP", + "associatedContractCount":2 + } + ] + }, + + "dcnm_protocols_get_one_resp": { + "RETURN_CODE":200, + "METHOD":"GET", + "REQUEST_PATH":"https://192.168.2.1:443/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/protocols/test", + "MESSAGE":"OK", + "DATA": + { + "fabricName":"test_fab", + "protocolName":"test", + "matchType":"any", + "matchItems":[ + { + "type":"IP v4", + "protocolOptions":"TCP" + } + ], + "matchSummary":"IPv4 TCP", + "associatedContractCount":2 + } + }, + + "dcnm_protocols_conf_delete_resp": { + "DATA": { + "code": "200", + "failedCount": 0, + "failureList": [], + "message": "success", + "successCount": 2, + "successList": [ + { + "code": "200", + "message": "test delete successful", + "name": "test", + "resourceId": "{\"protocolName\":\"test\"}", + "status": "deleted" + } + ], + "totalCount": 2 + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://192.168.2.1:443/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/protocols/bulkDelete", + "RETURN_CODE": 200 + }, + + "dcnm_protocols_delete_resp": { + "DATA": { + "code": "200", + "failedCount": 0, + "failureList": [], + "message": "success", + "successCount": 2, + "successList": [ + { + "code": "200", + "message": "test delete successful", + "name": "test", + "resourceId": "{\"protocolName\":\"test\"}", + "status": "deleted" + }, + { + "code": "200", + "message": "http delete successful", + "name": "http", + "resourceId": "{\"protocolName\":\"test1\"}", + "status": "deleted" + } + ], + "totalCount": 2 + }, + "MESSAGE": "OK", + "METHOD": "POST", + "REQUEST_PATH": "https://10.78.210.227:443/appcenter/cisco/ndfc/api/v1/security/fabrics/test_fab/protocols/bulkDelete", + "RETURN_CODE": 200 + } +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/test_dcnm_contracts.py b/tests/unit/modules/dcnm/test_dcnm_contracts.py new file mode 100644 index 000000000..cbe5c3e98 --- /dev/null +++ b/tests/unit/modules/dcnm/test_dcnm_contracts.py @@ -0,0 +1,656 @@ +# 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__ = "Praveen Ramoorthy" + +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_contracts_utils +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm import dcnm +from ansible_collections.cisco.dcnm.plugins.modules import dcnm_contracts +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm_contracts_utils import ( + dcnm_contracts_get_paths as contracts_paths, +) +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_contracts import DcnmContracts + +# Importing Fixtures +from .fixtures.dcnm_contracts.dcnm_contracts_common import dcnm_contracts_fixture + +from unittest.mock import Mock + +# Fixtures path +fixture_path = os.path.join(os.path.dirname(__file__), "fixtures") +module_data_path = fixture_path + "/dcnm_contracts/" + + +# UNIT TEST CASES +def load_data(module_name): + 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 + + +def test_dcnm_contracts_log_msg(monkeypatch, dcnm_contracts_fixture): + + # Testing Function log_msg() + + contracts = dcnm_contracts_fixture + contracts.log_msg("This is a test message to test logging function\n") + + try: + os.remove("dcnm_contracts.log") + except Exception as e: + print(str(e)) + + +class TestDcnmContractsModule(TestDcnmModule): + + module = dcnm_contracts + + fd = None + + def setUp(self): + super(TestDcnmContractsModule, self).setUp() + self.monkeypatch = MonkeyPatch() + + def test_dcnm_contracts_create_new(self): + + # Testing Function for creating new contracts + data = load_data("dcnm_contracts_data") + resp = load_data("dcnm_contracts_response") + + dcnm_version_supported_side_effect = [] + get_fabric_inventory_details_side_effect = [] + get_fabric_details_side_effect = [] + dcnm_send_side_effect = [] + + dcnm_version_supported_side_effect.append(12) + get_fabric_inventory_details_side_effect.append(resp.get("contracts_inventory_details")) + get_fabric_details_side_effect.append(resp.get("contracts_fabric_details")) + + mock_dcnm_version_supported = Mock( + side_effect=dcnm_version_supported_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "dcnm_version_supported", mock_dcnm_version_supported + ) + + mock_get_fabric_inventory_details = Mock( + side_effect=get_fabric_inventory_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, + "get_fabric_inventory_details", + mock_get_fabric_inventory_details, + ) + + mock_get_fabric_details = Mock( + side_effect=get_fabric_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "get_fabric_details", mock_get_fabric_details + ) + + # dcnm_send invocation from module_utils/dcnm_contracts_utils.py + dcnm_send_side_effect.append(resp.get("dcnm_contracts_empty_resp")) + dcnm_send_side_effect.append(resp.get("dcnm_contracts_create_resp")) + + mock_dcnm_send = Mock(side_effect=dcnm_send_side_effect) + self.monkeypatch.setattr(dcnm_contracts_utils, "dcnm_send", mock_dcnm_send) + + config = data.get("contracts_cfg_00001") + playbook_config = [config] + + set_module_args( + dict( + state="merged", + fabric="test_fab", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + assert result.get("changed") is True + self.assertEqual(len(result["diff"][0]["merged"]), 1) + self.assertEqual(result["diff"][0]["merged"][0]["contractName"], "test") + self.assertEqual(result["diff"][0]["merged"][0]["rules"][0]["protocolName"], "http") + + def test_dcnm_contracts_modify_existing(self): + + # Testing Function for modifying existing contracts + data = load_data("dcnm_contracts_data") + resp = load_data("dcnm_contracts_response") + + dcnm_version_supported_side_effect = [] + get_fabric_inventory_details_side_effect = [] + get_fabric_details_side_effect = [] + dcnm_send_side_effect = [] + + dcnm_version_supported_side_effect.append(12) + get_fabric_inventory_details_side_effect.append(resp.get("contracts_inventory_details")) + get_fabric_details_side_effect.append(resp.get("contracts_fabric_details")) + + mock_dcnm_version_supported = Mock( + side_effect=dcnm_version_supported_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "dcnm_version_supported", mock_dcnm_version_supported + ) + + mock_get_fabric_inventory_details = Mock( + side_effect=get_fabric_inventory_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, + "get_fabric_inventory_details", + mock_get_fabric_inventory_details, + ) + + mock_get_fabric_details = Mock( + side_effect=get_fabric_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "get_fabric_details", mock_get_fabric_details + ) + + # dcnm_send invocation from module_utils/dcnm_contracts_utils.py + dcnm_send_side_effect.append(resp.get("dcnm_contracts_get_resp")) + dcnm_send_side_effect.append(resp.get("dcnm_contracts_modify_resp")) + + mock_dcnm_send = Mock(side_effect=dcnm_send_side_effect) + self.monkeypatch.setattr(dcnm_contracts_utils, "dcnm_send", mock_dcnm_send) + + config = data.get("contracts_cfg_00002") + playbook_config = [config] + + set_module_args( + dict( + state="merged", + fabric="test_fab", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + assert result.get("changed") is True + self.assertEqual(len(result["diff"][0]["modified"]), 1) + self.assertEqual(result["diff"][0]["modified"][0]["contractName"], "test") + self.assertEqual(result["diff"][0]["modified"][0]["rules"][0]["protocolName"], "http") + self.assertEqual(result["diff"][0]["modified"][0]["rules"][1]["protocolName"], "test1") + + def test_dcnm_contracts_delete_existing_with_config(self): + + # Testing Function for deleting existing contracts + data = load_data("dcnm_contracts_data") + resp = load_data("dcnm_contracts_response") + + dcnm_version_supported_side_effect = [] + get_fabric_inventory_details_side_effect = [] + get_fabric_details_side_effect = [] + dcnm_send_side_effect = [] + + dcnm_version_supported_side_effect.append(12) + get_fabric_inventory_details_side_effect.append(resp.get("contracts_inventory_details")) + get_fabric_details_side_effect.append(resp.get("contracts_fabric_details")) + + mock_dcnm_version_supported = Mock( + side_effect=dcnm_version_supported_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "dcnm_version_supported", mock_dcnm_version_supported + ) + + mock_get_fabric_inventory_details = Mock( + side_effect=get_fabric_inventory_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, + "get_fabric_inventory_details", + mock_get_fabric_inventory_details, + ) + + mock_get_fabric_details = Mock( + side_effect=get_fabric_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "get_fabric_details", mock_get_fabric_details + ) + + # dcnm_send invocation from module_utils/dcnm_contracts_utils.py + dcnm_send_side_effect.append(resp.get("dcnm_contracts_get_one_resp")) + dcnm_send_side_effect.append(resp.get("dcnm_contracts_conf_delete_resp")) + + mock_dcnm_send = Mock(side_effect=dcnm_send_side_effect) + self.monkeypatch.setattr(dcnm_contracts_utils, "dcnm_send", mock_dcnm_send) + + config = data.get("contracts_cfg_00001") + playbook_config = [config] + + set_module_args( + dict( + state="deleted", + fabric="test_fab", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + assert result.get("changed") is True + self.assertEqual(len(result["diff"][0]["deleted"]), 1) + self.assertEqual(result["diff"][0]["deleted"][0], "test") + + def test_dcnm_contracts_delete_existing_without_config(self): + + # Testing Function for deleting all existing contracts + data = load_data("dcnm_contracts_data") + resp = load_data("dcnm_contracts_response") + + dcnm_version_supported_side_effect = [] + get_fabric_inventory_details_side_effect = [] + get_fabric_details_side_effect = [] + dcnm_send_side_effect = [] + + dcnm_version_supported_side_effect.append(12) + get_fabric_inventory_details_side_effect.append(resp.get("contracts_inventory_details")) + get_fabric_details_side_effect.append(resp.get("contracts_fabric_details")) + + mock_dcnm_version_supported = Mock( + side_effect=dcnm_version_supported_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "dcnm_version_supported", mock_dcnm_version_supported + ) + + mock_get_fabric_inventory_details = Mock( + side_effect=get_fabric_inventory_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, + "get_fabric_inventory_details", + mock_get_fabric_inventory_details, + ) + + mock_get_fabric_details = Mock( + side_effect=get_fabric_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "get_fabric_details", mock_get_fabric_details + ) + + # dcnm_send invocation from module_utils/dcnm_contracts_utils.py + dcnm_send_side_effect.append(resp.get("dcnm_contracts_get_resp")) + dcnm_send_side_effect.append(resp.get("dcnm_contracts_delete_resp")) + + mock_dcnm_send = Mock(side_effect=dcnm_send_side_effect) + self.monkeypatch.setattr(dcnm_contracts_utils, "dcnm_send", mock_dcnm_send) + + set_module_args( + dict( + state="deleted", + fabric="test_fab", + ) + ) + + result = self.execute_module(changed=True, failed=False) + + assert result.get("changed") is True + self.assertEqual(len(result["diff"][0]["deleted"]), 2) + self.assertEqual(result["diff"][0]["deleted"][0], "test1") + self.assertEqual(result["diff"][0]["deleted"][1], "test") + + def test_dcnm_contracts_replace_existing(self): + + # Testing Function for replacing existing contracts + data = load_data("dcnm_contracts_data") + resp = load_data("dcnm_contracts_response") + + dcnm_version_supported_side_effect = [] + get_fabric_inventory_details_side_effect = [] + get_fabric_details_side_effect = [] + dcnm_send_side_effect = [] + + dcnm_version_supported_side_effect.append(12) + get_fabric_inventory_details_side_effect.append(resp.get("contracts_inventory_details")) + get_fabric_details_side_effect.append(resp.get("contracts_fabric_details")) + + mock_dcnm_version_supported = Mock( + side_effect=dcnm_version_supported_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "dcnm_version_supported", mock_dcnm_version_supported + ) + + mock_get_fabric_inventory_details = Mock( + side_effect=get_fabric_inventory_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, + "get_fabric_inventory_details", + mock_get_fabric_inventory_details, + ) + + mock_get_fabric_details = Mock( + side_effect=get_fabric_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "get_fabric_details", mock_get_fabric_details + ) + + # dcnm_send invocation from module_utils/dcnm_contracts_utils.py + dcnm_send_side_effect.append(resp.get("dcnm_contracts_get_resp")) + dcnm_send_side_effect.append(resp.get("dcnm_contracts_modify_resp")) + + mock_dcnm_send = Mock(side_effect=dcnm_send_side_effect) + self.monkeypatch.setattr(dcnm_contracts_utils, "dcnm_send", mock_dcnm_send) + + config = data.get("contracts_cfg_00003") + playbook_config = [config] + + set_module_args( + dict( + state="replaced", + fabric="test_fab", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + assert result.get("changed") is True + self.assertEqual(len(result["diff"][0]["modified"]), 1) + self.assertEqual(result["diff"][0]["modified"][0]["contractName"], "test") + self.assertEqual(result["diff"][0]["modified"][0]["rules"][0]["protocolName"], "test") + + def test_dcnm_contracts_override_exiting(self): + + # Testing Function for overriding existing contracts + data = load_data("dcnm_contracts_data") + resp = load_data("dcnm_contracts_response") + + dcnm_version_supported_side_effect = [] + get_fabric_inventory_details_side_effect = [] + get_fabric_details_side_effect = [] + dcnm_send_side_effect = [] + + dcnm_version_supported_side_effect.append(12) + get_fabric_inventory_details_side_effect.append(resp.get("contracts_inventory_details")) + get_fabric_details_side_effect.append(resp.get("contracts_fabric_details")) + + mock_dcnm_version_supported = Mock( + side_effect=dcnm_version_supported_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "dcnm_version_supported", mock_dcnm_version_supported + ) + + mock_get_fabric_inventory_details = Mock( + side_effect=get_fabric_inventory_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, + "get_fabric_inventory_details", + mock_get_fabric_inventory_details, + ) + + mock_get_fabric_details = Mock( + side_effect=get_fabric_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "get_fabric_details", mock_get_fabric_details + ) + + # dcnm_send invocation from module_utils/dcnm_contracts_utils.py + dcnm_send_side_effect.append(resp.get("dcnm_contracts_get_resp")) + dcnm_send_side_effect.append(resp.get("dcnm_contracts_get_resp")) + dcnm_send_side_effect.append(resp.get("dcnm_contracts_conf_delete_resp")) + dcnm_send_side_effect.append(resp.get("dcnm_contracts_modify_resp")) + + mock_dcnm_send = Mock(side_effect=dcnm_send_side_effect) + self.monkeypatch.setattr(dcnm_contracts_utils, "dcnm_send", mock_dcnm_send) + + config = data.get("contracts_cfg_00003") + playbook_config = [config] + + set_module_args( + dict( + state="overridden", + fabric="test_fab", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + assert result.get("changed") is True + self.assertEqual(len(result["diff"][0]["deleted"]), 1) + self.assertEqual(result["diff"][0]["deleted"][0], "test1") + self.assertEqual(len(result["diff"][0]["modified"]), 1) + self.assertEqual(result["diff"][0]["modified"][0]["contractName"], "test") + + def test_dcnm_contracts_query_config(self): + + # Testing Function for querying contracts config + data = load_data("dcnm_contracts_data") + resp = load_data("dcnm_contracts_response") + + dcnm_version_supported_side_effect = [] + get_fabric_inventory_details_side_effect = [] + get_fabric_details_side_effect = [] + dcnm_send_side_effect = [] + + dcnm_version_supported_side_effect.append(12) + get_fabric_inventory_details_side_effect.append(resp.get("contracts_inventory_details")) + get_fabric_details_side_effect.append(resp.get("contracts_fabric_details")) + + mock_dcnm_version_supported = Mock( + side_effect=dcnm_version_supported_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "dcnm_version_supported", mock_dcnm_version_supported + ) + + mock_get_fabric_inventory_details = Mock( + side_effect=get_fabric_inventory_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, + "get_fabric_inventory_details", + mock_get_fabric_inventory_details, + ) + + mock_get_fabric_details = Mock( + side_effect=get_fabric_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "get_fabric_details", mock_get_fabric_details + ) + + # dcnm_send invocation from module_utils/dcnm_contracts_utils.py + dcnm_send_side_effect.append(resp.get("dcnm_contracts_get_resp")) + + mock_dcnm_send = Mock(side_effect=dcnm_send_side_effect) + self.monkeypatch.setattr(dcnm_contracts_utils, "dcnm_send", mock_dcnm_send) + + config = data.get("contracts_cfg_00001") + playbook_config = [config] + + set_module_args( + dict( + state="query", + fabric="test_fab", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + assert result.get("changed") is False + self.assertEqual(len(result["response"]), 1) + self.assertEqual(result["response"][0]["contractName"], "test") + + def test_dcnm_contracts_query_all(self): + + # Testing Function for querying all contracts + data = load_data("dcnm_contracts_data") + resp = load_data("dcnm_contracts_response") + + dcnm_version_supported_side_effect = [] + get_fabric_inventory_details_side_effect = [] + get_fabric_details_side_effect = [] + dcnm_send_side_effect = [] + + dcnm_version_supported_side_effect.append(12) + get_fabric_inventory_details_side_effect.append(resp.get("contracts_inventory_details")) + get_fabric_details_side_effect.append(resp.get("contracts_fabric_details")) + + mock_dcnm_version_supported = Mock( + side_effect=dcnm_version_supported_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "dcnm_version_supported", mock_dcnm_version_supported + ) + + mock_get_fabric_inventory_details = Mock( + side_effect=get_fabric_inventory_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, + "get_fabric_inventory_details", + mock_get_fabric_inventory_details, + ) + + mock_get_fabric_details = Mock( + side_effect=get_fabric_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "get_fabric_details", mock_get_fabric_details + ) + + # dcnm_send invocation from module_utils/dcnm_contracts_utils.py + dcnm_send_side_effect.append(resp.get("dcnm_contracts_get_resp")) + + mock_dcnm_send = Mock(side_effect=dcnm_send_side_effect) + self.monkeypatch.setattr(dcnm_contracts_utils, "dcnm_send", mock_dcnm_send) + + set_module_args( + dict( + state="query", + fabric="test_fab", + ) + ) + + result = self.execute_module(changed=False, failed=False) + + assert result.get("changed") is False + self.assertEqual(len(result["response"]), 2) + self.assertEqual(result["response"][0]["contractName"], "test1") + self.assertEqual(result["response"][1]["contractName"], "test") + + def test_dcnm_contracts_merge_check_mode(self): + + # Testing Function for merging contracts in check mode + data = load_data("dcnm_contracts_data") + resp = load_data("dcnm_contracts_response") + + dcnm_version_supported_side_effect = [] + get_fabric_inventory_details_side_effect = [] + get_fabric_details_side_effect = [] + dcnm_send_side_effect = [] + + dcnm_version_supported_side_effect.append(12) + get_fabric_inventory_details_side_effect.append(resp.get("contracts_inventory_details")) + get_fabric_details_side_effect.append(resp.get("contracts_fabric_details")) + + mock_dcnm_version_supported = Mock( + side_effect=dcnm_version_supported_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "dcnm_version_supported", mock_dcnm_version_supported + ) + + mock_get_fabric_inventory_details = Mock( + side_effect=get_fabric_inventory_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, + "get_fabric_inventory_details", + mock_get_fabric_inventory_details, + ) + + mock_get_fabric_details = Mock( + side_effect=get_fabric_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_contracts, "get_fabric_details", mock_get_fabric_details + ) + + # dcnm_send invocation from module_utils/dcnm_contracts_utils.py + dcnm_send_side_effect.append(resp.get("dcnm_contracts_empty_resp")) + dcnm_send_side_effect.append(resp.get("dcnm_contracts_create_resp")) + + mock_dcnm_send = Mock(side_effect=dcnm_send_side_effect) + self.monkeypatch.setattr(dcnm_contracts_utils, "dcnm_send", mock_dcnm_send) + + config = data.get("contracts_cfg_00001") + playbook_config = [config] + + set_module_args( + dict( + state="merged", + fabric="test_fab", + config=playbook_config, + _ansible_check_mode=True, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + assert result.get("changed") is False + self.assertEqual(len(result["diff"][0]["merged"]), 1) + self.assertEqual(result["diff"][0]["merged"][0]["contractName"], "test") diff --git a/tests/unit/modules/dcnm/test_dcnm_protocols.py b/tests/unit/modules/dcnm/test_dcnm_protocols.py new file mode 100644 index 000000000..08d8f3073 --- /dev/null +++ b/tests/unit/modules/dcnm/test_dcnm_protocols.py @@ -0,0 +1,527 @@ +# 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__ = "Praveen Ramoorthy" + +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_protocols_utils +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm import dcnm +from ansible_collections.cisco.dcnm.plugins.modules import dcnm_protocols +from ansible_collections.cisco.dcnm.plugins.module_utils.network.dcnm.dcnm_protocols_utils import ( + dcnm_protocols_get_paths as protocols_paths, +) +from ansible_collections.cisco.dcnm.plugins.modules.dcnm_protocols import DcnmProtocols + +# Importing Fixtures +from .fixtures.dcnm_protocols.dcnm_protocols_common import dcnm_protocols_fixture + +from unittest.mock import Mock + +# Fixtures path +fixture_path = os.path.join(os.path.dirname(__file__), "fixtures") +module_data_path = fixture_path + "/dcnm_protocols/" + + +# UNIT TEST CASES + +def load_data(module_name): + 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 + + +def test_dcnm_protocols_log_msg(monkeypatch, dcnm_protocols_fixture): + + # Testing Function log_msg() + + protocols = dcnm_protocols_fixture + protocols.log_msg("This is a test message to test logging function\n") + + try: + os.remove("dcnm_protocols.log") + except Exception as e: + print(str(e)) + + +class TestDcnmProtocolsModule(TestDcnmModule): + + module = dcnm_protocols + + fd = None + + def setUp(self): + super(TestDcnmProtocolsModule, self).setUp() + self.monkeypatch = MonkeyPatch() + + def test_dcnm_protocols_create_new(self): + + # Testing function for create new protocol + data = load_data("dcnm_protocols_data") + resp = load_data("dcnm_protocols_response") + + dcnm_version_supported_side_effect = [] + get_fabric_inventory_details_side_effect = [] + get_fabric_details_side_effect = [] + dcnm_send_side_effect = [] + + dcnm_version_supported_side_effect.append(12) + get_fabric_inventory_details_side_effect.append(resp.get("protocols_inventory_details")) + get_fabric_details_side_effect.append(resp.get("protocols_fabric_details")) + + mock_dcnm_version_supported = Mock( + side_effect=dcnm_version_supported_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, "dcnm_version_supported", mock_dcnm_version_supported + ) + + mock_get_fabric_inventory_details = Mock( + side_effect=get_fabric_inventory_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, + "get_fabric_inventory_details", + mock_get_fabric_inventory_details, + ) + + mock_get_fabric_details = Mock( + side_effect=get_fabric_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, "get_fabric_details", mock_get_fabric_details + ) + + # dcnm_send invocation from module_utils/dcnm_protocols_utils.py + dcnm_send_side_effect.append(resp.get("dcnm_protocols_empty_resp")) + dcnm_send_side_effect.append(resp.get("dcnm_protocols_create_resp")) + + mock_dcnm_send = Mock(side_effect=dcnm_send_side_effect) + self.monkeypatch.setattr(dcnm_protocols_utils, "dcnm_send", mock_dcnm_send) + + config = data.get("protocols_cfg_00001") + playbook_config = [config] + + set_module_args( + dict( + state="merged", + fabric="test_fab", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + assert result.get("changed") is True + self.assertEqual(len(result["diff"][0]["merged"]), 1) + self.assertEqual(result["diff"][0]["merged"][0]["protocolName"], "test") + self.assertEqual(result["diff"][0]["merged"][0]["matchItems"][0]["type"], "ip") + + def test_dcnm_protocols_modify_existing(self): + + # Testing function for modify existing protocol + data = load_data("dcnm_protocols_data") + resp = load_data("dcnm_protocols_response") + + dcnm_version_supported_side_effect = [] + get_fabric_inventory_details_side_effect = [] + get_fabric_details_side_effect = [] + dcnm_send_side_effect = [] + + dcnm_version_supported_side_effect.append(12) + get_fabric_inventory_details_side_effect.append(resp.get("protocols_inventory_details")) + get_fabric_details_side_effect.append(resp.get("protocols_fabric_details")) + + mock_dcnm_version_supported = Mock( + side_effect=dcnm_version_supported_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, "dcnm_version_supported", mock_dcnm_version_supported + ) + + mock_get_fabric_inventory_details = Mock( + side_effect=get_fabric_inventory_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, + "get_fabric_inventory_details", + mock_get_fabric_inventory_details, + ) + + mock_get_fabric_details = Mock( + side_effect=get_fabric_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, "get_fabric_details", mock_get_fabric_details + ) + + # dcnm_send invocation from module_utils/dcnm_protocols_utils.py + dcnm_send_side_effect.append(resp.get("dcnm_protocols_get_resp")) + dcnm_send_side_effect.append(resp.get("dcnm_protocols_modify_resp")) + + mock_dcnm_send = Mock(side_effect=dcnm_send_side_effect) + self.monkeypatch.setattr(dcnm_protocols_utils, "dcnm_send", mock_dcnm_send) + + config = data.get("protocols_cfg_00002") + playbook_config = [config] + + set_module_args( + dict( + state="merged", + fabric="test_fab", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + assert result.get("changed") is True + self.assertEqual(len(result["diff"][0]["modified"]), 1) + self.assertEqual(result["diff"][0]["modified"][0]["protocolName"], "test") + + def test_dcnm_protocols_delete_existing_with_config(self): + + # Testing function for delete existing protocol with config + data = load_data("dcnm_protocols_data") + resp = load_data("dcnm_protocols_response") + + dcnm_version_supported_side_effect = [] + get_fabric_inventory_details_side_effect = [] + get_fabric_details_side_effect = [] + dcnm_send_side_effect = [] + + dcnm_version_supported_side_effect.append(12) + get_fabric_inventory_details_side_effect.append(resp.get("protocols_inventory_details")) + get_fabric_details_side_effect.append(resp.get("protocols_fabric_details")) + + mock_dcnm_version_supported = Mock( + side_effect=dcnm_version_supported_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, "dcnm_version_supported", mock_dcnm_version_supported + ) + + mock_get_fabric_inventory_details = Mock( + side_effect=get_fabric_inventory_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, + "get_fabric_inventory_details", + mock_get_fabric_inventory_details, + ) + + mock_get_fabric_details = Mock( + side_effect=get_fabric_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, "get_fabric_details", mock_get_fabric_details + ) + + # dcnm_send invocation from module_utils/dcnm_protocols_utils.py + dcnm_send_side_effect.append(resp.get("dcnm_protocols_get_one_resp")) + dcnm_send_side_effect.append(resp.get("dcnm_protocols_delete_resp")) + + mock_dcnm_send = Mock(side_effect=dcnm_send_side_effect) + self.monkeypatch.setattr(dcnm_protocols_utils, "dcnm_send", mock_dcnm_send) + + config = data.get("protocols_cfg_00001") + playbook_config = [config] + + set_module_args( + dict( + state="deleted", + fabric="test_fab", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + assert result.get("changed") is True + self.assertEqual(len(result["diff"][0]["deleted"]), 1) + self.assertEqual(result["diff"][0]["deleted"][0], "test") + + def test_dcnm_protocols_delete_existing_without_config(self): + + # Testing function for delete existing protocols without config + data = load_data("dcnm_protocols_data") + resp = load_data("dcnm_protocols_response") + + dcnm_version_supported_side_effect = [] + get_fabric_inventory_details_side_effect = [] + get_fabric_details_side_effect = [] + dcnm_send_side_effect = [] + + dcnm_version_supported_side_effect.append(12) + get_fabric_inventory_details_side_effect.append(resp.get("protocols_inventory_details")) + get_fabric_details_side_effect.append(resp.get("protocols_fabric_details")) + + mock_dcnm_version_supported = Mock( + side_effect=dcnm_version_supported_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, "dcnm_version_supported", mock_dcnm_version_supported + ) + + mock_get_fabric_inventory_details = Mock( + side_effect=get_fabric_inventory_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, + "get_fabric_inventory_details", + mock_get_fabric_inventory_details, + ) + + mock_get_fabric_details = Mock( + side_effect=get_fabric_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, "get_fabric_details", mock_get_fabric_details + ) + + # dcnm_send invocation from module_utils/dcnm_protocols_utils.py + dcnm_send_side_effect.append(resp.get("dcnm_protocols_get_resp")) + dcnm_send_side_effect.append(resp.get("dcnm_protocols_delete_resp")) + + mock_dcnm_send = Mock(side_effect=dcnm_send_side_effect) + self.monkeypatch.setattr(dcnm_protocols_utils, "dcnm_send", mock_dcnm_send) + + set_module_args( + dict( + state="deleted", + fabric="test_fab", + config=[], + ) + ) + + result = self.execute_module(changed=True, failed=False) + + assert result.get("changed") is True + self.assertEqual(len(result["diff"][0]["deleted"]), 2) + self.assertEqual(result["diff"][0]["deleted"][0], "test") + self.assertEqual(result["diff"][0]["deleted"][1], "test1") + + def test_dcnm_protocols_replace_existing(self): + + # Testing function for replace existing protocols + data = load_data("dcnm_protocols_data") + resp = load_data("dcnm_protocols_response") + + dcnm_version_supported_side_effect = [] + get_fabric_inventory_details_side_effect = [] + get_fabric_details_side_effect = [] + dcnm_send_side_effect = [] + + dcnm_version_supported_side_effect.append(12) + get_fabric_inventory_details_side_effect.append(resp.get("protocols_inventory_details")) + get_fabric_details_side_effect.append(resp.get("protocols_fabric_details")) + + mock_dcnm_version_supported = Mock( + side_effect=dcnm_version_supported_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, "dcnm_version_supported", mock_dcnm_version_supported + ) + + mock_get_fabric_inventory_details = Mock( + side_effect=get_fabric_inventory_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, + "get_fabric_inventory_details", + mock_get_fabric_inventory_details, + ) + + mock_get_fabric_details = Mock( + side_effect=get_fabric_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, "get_fabric_details", mock_get_fabric_details + ) + + # dcnm_send invocation from module_utils/dcnm_protocols_utils.py + dcnm_send_side_effect.append(resp.get("dcnm_protocols_get_resp")) + dcnm_send_side_effect.append(resp.get("dcnm_protocols_modify_resp")) + + mock_dcnm_send = Mock(side_effect=dcnm_send_side_effect) + self.monkeypatch.setattr(dcnm_protocols_utils, "dcnm_send", mock_dcnm_send) + + config = data.get("protocols_cfg_00003") + playbook_config = [config] + + set_module_args( + dict( + state="replaced", + fabric="test_fab", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=True, failed=False) + + assert result.get("changed") is True + self.assertEqual(len(result["diff"][0]["modified"]), 1) + self.assertEqual(result["diff"][0]["modified"][0]["protocolName"], "test") + self.assertEqual(result["diff"][0]["modified"][0]["matchItems"][0]["type"], "ipv6") + + def test_dcnm_protocols_query_config(self): + + # Testing function for query config + data = load_data("dcnm_protocols_data") + resp = load_data("dcnm_protocols_response") + + dcnm_version_supported_side_effect = [] + get_fabric_inventory_details_side_effect = [] + get_fabric_details_side_effect = [] + dcnm_send_side_effect = [] + + dcnm_version_supported_side_effect.append(12) + get_fabric_inventory_details_side_effect.append(resp.get("protocols_inventory_details")) + get_fabric_details_side_effect.append(resp.get("protocols_fabric_details")) + + mock_dcnm_version_supported = Mock( + side_effect=dcnm_version_supported_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, "dcnm_version_supported", mock_dcnm_version_supported + ) + + mock_get_fabric_inventory_details = Mock( + side_effect=get_fabric_inventory_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, + "get_fabric_inventory_details", + mock_get_fabric_inventory_details, + ) + + mock_get_fabric_details = Mock( + side_effect=get_fabric_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, "get_fabric_details", mock_get_fabric_details + ) + + # dcnm_send invocation from module_utils/dcnm_protocols_utils.py + dcnm_send_side_effect.append(resp.get("dcnm_protocols_get_resp")) + + mock_dcnm_send = Mock(side_effect=dcnm_send_side_effect) + self.monkeypatch.setattr(dcnm_protocols_utils, "dcnm_send", mock_dcnm_send) + + config = data.get("protocols_cfg_00001") + playbook_config = [config] + + set_module_args( + dict( + state="query", + fabric="test_fab", + config=playbook_config, + ) + ) + + result = self.execute_module(changed=False, failed=False) + + assert result.get("changed") is False + self.assertEqual(len(result["response"]), 1) + self.assertEqual(result["response"][0]["protocolName"], "test") + + def test_dcnm_protocols_query_all(self): + + # Testing function for query all + data = load_data("dcnm_protocols_data") + resp = load_data("dcnm_protocols_response") + + dcnm_version_supported_side_effect = [] + get_fabric_inventory_details_side_effect = [] + get_fabric_details_side_effect = [] + dcnm_send_side_effect = [] + + dcnm_version_supported_side_effect.append(12) + get_fabric_inventory_details_side_effect.append(resp.get("protocols_inventory_details")) + get_fabric_details_side_effect.append(resp.get("protocols_fabric_details")) + + mock_dcnm_version_supported = Mock( + side_effect=dcnm_version_supported_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, "dcnm_version_supported", mock_dcnm_version_supported + ) + + mock_get_fabric_inventory_details = Mock( + side_effect=get_fabric_inventory_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, + "get_fabric_inventory_details", + mock_get_fabric_inventory_details, + ) + + mock_get_fabric_details = Mock( + side_effect=get_fabric_details_side_effect + ) + self.monkeypatch.setattr( + dcnm_protocols, "get_fabric_details", mock_get_fabric_details + ) + + # dcnm_send invocation from module_utils/dcnm_protocols_utils.py + dcnm_send_side_effect.append(resp.get("dcnm_protocols_get_resp")) + + mock_dcnm_send = Mock(side_effect=dcnm_send_side_effect) + self.monkeypatch.setattr(dcnm_protocols_utils, "dcnm_send", mock_dcnm_send) + + set_module_args( + dict( + state="query", + fabric="test_fab", + ) + ) + + result = self.execute_module(changed=False, failed=False) + + assert result.get("changed") is False + self.assertEqual(len(result["response"]), 2) + self.assertEqual(result["response"][0]["protocolName"], "test") + self.assertEqual(result["response"][1]["protocolName"], "test1") From 15abe822d5514625d90a0333b90ee6a0e812b75d Mon Sep 17 00:00:00 2001 From: Praveen Ramoorthy Date: Tue, 15 Oct 2024 13:04:19 +0530 Subject: [PATCH 2/6] DCNM Security Groups Protocols and Contracts modules --- tests/sanity/ignore-2.15.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt index a66261d9e..b89f85cfc 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -19,6 +19,5 @@ plugins/modules/dcnm_fabric.py validate-modules:missing-gplv3-license # GPLv3 li 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_contracts.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_protocols.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 From 3c9144c24a55b2c8e0cb67e2f4da84bfc6492919 Mon Sep 17 00:00:00 2001 From: Praveen Ramoorthy Date: Wed, 6 Nov 2024 17:23:05 +0530 Subject: [PATCH 3/6] Logging support --- plugins/modules/dcnm_contracts.py | 48 ++++++++++++++++++++++++++++++- plugins/modules/dcnm_protocols.py | 46 +++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/plugins/modules/dcnm_contracts.py b/plugins/modules/dcnm_contracts.py index b960770dd..b380cc1ca 100644 --- a/plugins/modules/dcnm_contracts.py +++ b/plugins/modules/dcnm_contracts.py @@ -219,8 +219,10 @@ state: 'query' """ 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.network.dcnm.dcnm import ( validate_list_of_dicts, dcnm_version_supported, @@ -253,6 +255,8 @@ class DcnmContracts: 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"] @@ -420,7 +424,8 @@ def dcnm_contracts_get_diff_merge(self): for elem in self.want: rc, have = dcnm_contracts_utils_compare_want_and_have(self, elem) - + msg = f"Compare Want and Have: Return Code = {rc}, Have = {have}\n" + self.log.info(msg) if rc == "NDFC_CONTRACTS_CREATE": # Object does not exists, create a new one. if elem not in self.diff_create: @@ -646,6 +651,9 @@ def dcnm_contracts_send_message_to_dcnm(self): create_flag = dcnm_contracts_utils_process_create_payloads(self) modify_flag = dcnm_contracts_utils_process_modify_payloads(self) + msg = f"Flags: CR = {create_flag}, DL = {delete_flag}, MO = {modify_flag}\n" + self.log.debug(msg) + self.result["changed"] = ( create_flag or modify_flag or delete_flag ) @@ -696,6 +704,16 @@ def main(): state = module.params["state"] + # Logging setup + try: + log = Log() + log.commit() + except ValueError as error: + ansible_module.fail_json(str(error)) + + msg = f"######################### BEGIN STATE = {state} ##########################\n" + dcnm_contracts.log.debug(msg) + if [] is dcnm_contracts.config: if state == "merged" or state == "replaced": module.fail_json( @@ -706,14 +724,29 @@ def main(): dcnm_contracts.dcnm_contracts_validate_all_input() + msg = f"Config Info = {dcnm_contracts.config}\n" + dcnm_contracts.log.info(msg) + + msg = f"Validated Security Group Association Info = {dcnm_contracts.contracts_info}\n" + dcnm_contracts.log.info(msg) + if ( module.params["state"] != "query" and module.params["state"] != "deleted" ): dcnm_contracts.dcnm_contracts_get_want() + msg = f"Want = {dcnm_contracts.want}\n" + dcnm_contracts.log.info(msg) + dcnm_contracts.dcnm_contracts_get_have() + msg = f"Have = {dcnm_contracts.have}\n" + dcnm_contracts.log.info(msg) + + msg = f"Updated Want = {dcnm_contracts.want}\n" + dcnm_contracts.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' # state, objects not included in the playbook must be left as they are and for state 'replaced' @@ -733,6 +766,16 @@ def main(): if module.params["state"] == "query": dcnm_contracts.dcnm_contracts_get_diff_query() + + msg = f"Create Info = {dcnm_contracts.diff_create}\n" + dcnm_contracts.log.info(msg) + + msg = f"Replace Info = {dcnm_contracts.diff_modify}\n" + dcnm_contracts.log.info(msg) + + msg = f"Delete Info = {dcnm_contracts.diff_delete}\n" + dcnm_contracts.log.info(msg) + dcnm_contracts.result["diff"] = dcnm_contracts.changed_dict if dcnm_contracts.diff_create or dcnm_contracts.diff_delete or dcnm_contracts.diff_modify: @@ -744,6 +787,9 @@ def main(): dcnm_contracts.dcnm_contracts_send_message_to_dcnm() + msg = f"######################### END STATE = {state} ##########################\n" + dcnm_contracts.log.debug(msg) + module.exit_json(**dcnm_contracts.result) diff --git a/plugins/modules/dcnm_protocols.py b/plugins/modules/dcnm_protocols.py index 703c13db5..ccb6c9ade 100644 --- a/plugins/modules/dcnm_protocols.py +++ b/plugins/modules/dcnm_protocols.py @@ -242,8 +242,10 @@ """ 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.network.dcnm.dcnm import ( validate_list_of_dicts, dcnm_version_supported, @@ -277,6 +279,8 @@ class DcnmProtocols: def __init__(self, module): self.module = module self.params = module.params + self.class_name = self.__class__.__name__ + self.log = logging.getLogger(f"dcnm.{self.class_name}") self.fabric = module.params["fabric"] self.config = copy.deepcopy(module.params.get("config", [])) self.protocols_info = [] @@ -441,6 +445,8 @@ def dcnm_protocols_get_diff_merge(self): for elem in self.want: rc, have = dcnm_protocols_utils_compare_want_and_have(self, elem) + msg = f"Compare Want and Have: Return Code = {rc}, Have = {have}\n" + self.log.info(msg) if rc == "NDFC_PROTOCOLS_CREATE": # Object does not exists, create a new one. @@ -713,6 +719,9 @@ def dcnm_protocols_send_message_to_dcnm(self): create_flag = dcnm_protocols_utils_process_create_payloads(self) modify_flag = dcnm_protocols_utils_process_modify_payloads(self) + msg = f"Flags: CR = {create_flag}, DL = {delete_flag}, MO = {modify_flag}\n" + self.log.debug(msg) + self.result["changed"] = ( create_flag or modify_flag or delete_flag ) @@ -763,6 +772,16 @@ def main(): state = module.params["state"] + # Logging setup + try: + log = Log() + log.commit() + except ValueError as error: + ansible_module.fail_json(str(error)) + + msg = f"######################### BEGIN STATE = {state} ##########################\n" + dcnm_protocols.log.debug(msg) + if [] is dcnm_protocols.config: if state == "merged" or state == "replaced": module.fail_json( @@ -773,14 +792,26 @@ def main(): dcnm_protocols.dcnm_protocols_validate_all_input() + msg = f"Config Info = {dcnm_protocols.config}\n" + dcnm_protocols.log.info(msg) + + msg = f"Validated Security Group Association Info = {dcnm_protocols.protocols_info}\n" + dcnm_protocols.log.info(msg) + if ( module.params["state"] != "query" and module.params["state"] != "deleted" ): dcnm_protocols.dcnm_protocols_get_want() + msg = f"Want = {dcnm_protocols.want}\n" + dcnm_protocols.log.info(msg) + dcnm_protocols.dcnm_protocols_get_have() + msg = f"Have = {dcnm_protocols.have}\n" + dcnm_protocols.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' # state, objects not included in the playbook must be left as they are and for state 'replaced' @@ -791,6 +822,9 @@ def main(): ): dcnm_protocols.dcnm_protocols_get_diff_merge() + msg = f"Updated Want = {dcnm_protocols.want}\n" + dcnm_protocols.log.info(msg) + if module.params["state"] == "deleted": dcnm_protocols.dcnm_protocols_get_diff_deleted() @@ -800,6 +834,15 @@ def main(): if module.params["state"] == "query": dcnm_protocols.dcnm_protocols_get_diff_query() + msg = f"Create Info = {dcnm_protocols.diff_create}\n" + dcnm_protocols.log.info(msg) + + msg = f"Replace Info = {dcnm_protocols.diff_modify}\n" + dcnm_protocols.log.info(msg) + + msg = f"Delete Info = {dcnm_protocols.diff_delete}\n" + dcnm_protocols.log.info(msg) + dcnm_protocols.result["diff"] = dcnm_protocols.changed_dict if dcnm_protocols.diff_create or dcnm_protocols.diff_delete: @@ -811,6 +854,9 @@ def main(): dcnm_protocols.dcnm_protocols_send_message_to_dcnm() + msg = f"######################### END STATE = {state} ##########################\n" + dcnm_protocols.log.debug(msg) + module.exit_json(**dcnm_protocols.result) From e2f4e61713d98e21a51d11390aaddbfb5ed78726 Mon Sep 17 00:00:00 2001 From: Praveen Ramoorthy Date: Wed, 6 Nov 2024 18:17:46 +0530 Subject: [PATCH 4/6] Logging support --- plugins/modules/dcnm_contracts.py | 3 +-- plugins/modules/dcnm_protocols.py | 2 +- tests/sanity/ignore-2.14.txt | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/plugins/modules/dcnm_contracts.py b/plugins/modules/dcnm_contracts.py index b380cc1ca..929c8c850 100644 --- a/plugins/modules/dcnm_contracts.py +++ b/plugins/modules/dcnm_contracts.py @@ -709,7 +709,7 @@ def main(): log = Log() log.commit() except ValueError as error: - ansible_module.fail_json(str(error)) + module.fail_json(str(error)) msg = f"######################### BEGIN STATE = {state} ##########################\n" dcnm_contracts.log.debug(msg) @@ -766,7 +766,6 @@ def main(): if module.params["state"] == "query": dcnm_contracts.dcnm_contracts_get_diff_query() - msg = f"Create Info = {dcnm_contracts.diff_create}\n" dcnm_contracts.log.info(msg) diff --git a/plugins/modules/dcnm_protocols.py b/plugins/modules/dcnm_protocols.py index ccb6c9ade..262b692a0 100644 --- a/plugins/modules/dcnm_protocols.py +++ b/plugins/modules/dcnm_protocols.py @@ -777,7 +777,7 @@ def main(): log = Log() log.commit() except ValueError as error: - ansible_module.fail_json(str(error)) + module.fail_json(str(error)) msg = f"######################### BEGIN STATE = {state} ##########################\n" dcnm_protocols.log.debug(msg) diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt index 0951e7152..ec1bd8d0b 100644 --- a/tests/sanity/ignore-2.14.txt +++ b/tests/sanity/ignore-2.14.txt @@ -20,6 +20,5 @@ plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license plugins/modules/dcnm_contracts.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_protocols.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_bootflash.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 \ No newline at end of file +plugins/httpapi/dcnm.py import-3.10!skip From b92051582356d717eb192fb5b4a55cfff2e2ae3f Mon Sep 17 00:00:00 2001 From: Praveen Ramoorthy Date: Wed, 6 Nov 2024 18:57:00 +0530 Subject: [PATCH 5/6] Logging support --- tests/sanity/ignore-2.15.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt index 0951e7152..ec1bd8d0b 100644 --- a/tests/sanity/ignore-2.15.txt +++ b/tests/sanity/ignore-2.15.txt @@ -20,6 +20,5 @@ plugins/modules/dcnm_maintenance_mode.py validate-modules:missing-gplv3-license plugins/modules/dcnm_contracts.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_protocols.py validate-modules:missing-gplv3-license # GPLv3 license header not found in the first 20 lines of the module plugins/modules/dcnm_bootflash.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 \ No newline at end of file +plugins/httpapi/dcnm.py import-3.10!skip From 55e0b318c5865026e5078decb9267cdf667f4630 Mon Sep 17 00:00:00 2001 From: Praveen Ramoorthy Date: Thu, 7 Nov 2024 22:51:12 +0530 Subject: [PATCH 6/6] Logging support --- tests/unit/module_utils/common/test_log_v2.py | 1 + 1 file changed, 1 insertion(+) 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: