From d214e97fb9fb023401404d581ac625df27e130b2 Mon Sep 17 00:00:00 2001 From: Gaspard Micol Date: Fri, 11 Oct 2024 13:12:50 -0400 Subject: [PATCH 01/11] [minor_change] Add ndo_virtual_port_channel_interface as a new module. --- .../ndo_virtual_port_channel_interface.py | 383 +++++++++++++++++ .../aliases | 2 + .../tasks/main.yml | 401 ++++++++++++++++++ 3 files changed, 786 insertions(+) create mode 100644 plugins/modules/ndo_virtual_port_channel_interface.py create mode 100644 tests/integration/targets/ndo_virtual_port_channel_interface/aliases create mode 100644 tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml diff --git a/plugins/modules/ndo_virtual_port_channel_interface.py b/plugins/modules/ndo_virtual_port_channel_interface.py new file mode 100644 index 00000000..82c19002 --- /dev/null +++ b/plugins/modules/ndo_virtual_port_channel_interface.py @@ -0,0 +1,383 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2024, Gaspard Micol (@gmicol) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", +} + +DOCUMENTATION = r""" +--- +module: ndo_virtual_port_channel_interface +short_description: Manage Virtual Port Channel Interfaces_1 on Cisco Nexus Dashboard Orchestrator (NDO). +description: +- Manage Virtual Port Channel Interfaces_1 on Cisco Nexus Dashboard Orchestrator (NDO). +- This module is only supported on ND v3.2 (NDO v4.4) and later. +author: +- Gaspard Micol (@gmicol) +options: + template: + description: + - The name of the template. + - The template must be a fabric resource template. + type: str + required: true + virtual_port_channel_interface: + description: + - The name of the Virtual Port Channel Interface. + type: str + aliases: [ name, virtual_port_channel, vpc ] + virtual_port_channel_interface_uuid: + description: + - The uuid of the Virtual Port Channel Interface. + - This parameter is required when parameter O(virtual_port_channel_interface) is updated. + type: str + aliases: [ uuid, virtual_port_channel_uuid, vpc_uuid ] + description: + description: + - The description of the Port Channel Interface. + type: str + node_1: + description: + - The first node ID. + type: str + node_2: + description: + - The second node ID. + type: str + interfaces_1: + description: + - The list of used Interface IDs for the first node. + - Ranges of Interface IDs can be used. + type: list + elements: str + aliases: [ members_1 ] + interfaces_2: + description: + - The list of used Interface IDs for the second node. + - Ranges of Interface IDs can be used. + type: list + elements: str + aliases: [ members_2 ] + interface_policy_group_uuid: + description: + - The UUID of the Port Channel Interface Setting Policy. + type: str + aliases: [ policy_uuid, interface_policy_uuid ] + interface_policy_group: + description: + - The name of the Port Channel Interface Policy Group. + - This parameter can be used instead of O(interface_policy_group_uuid). + type: dict + suboptions: + name: + description: + - The name of the Port Channel Interface Setting Policy. + type: str + template: + description: + - The name of the template in which is referred the Port Channel Interface Policy Group. + type: str + aliases: [ policy, interface_policy ] + interface_descriptions: + description: + - The list of descriptions for each interface. + type: list + elements: dict + suboptions: + node: + description: + - The node ID + type: str + interface_id: + description: + - The interface ID. + type: str + description: + description: + - The description of the interface. + type: str + state: + description: + - Use C(absent) for removing. + - Use C(query) for listing an object or multiple objects. + - Use C(present) for creating or updating. + type: str + choices: [ absent, query, present ] + default: query +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Create a new Virtual Port Channel Interface + cisco.mso.ndo_virtual_port_channel_interface: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_resource_template + description: My Ansible Port Channel + virtual_port_channel_interface: ansible_virtual_port_channel_interface + node_1: 101 + node_2: 102 + interfaces_1: + - 1/1 + - 1/10-11 + interfaces_2: + - 1/2 + interface_policy_group: + name: ansible_policy_group + template: ansible_fabric_policy_template + interface_descriptions: + - node: 101 + interface_id: 1/1 + description: My first Ansible Interface for first node + - node: 101 + interface_id: 1/10 + description: My second Ansible Interface for first node + - node: 101 + interface_id: 1/11 + description: My third Ansible Interface for first node + - node: 102 + interface_id: 1/2 + description: My first Ansible Interface for second node + state: present + +- name: Query an Virtual Port Channel Interface with template_name + cisco.mso.ndo_virtual_port_channel_interface: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_resource_template + virtual_port_channel_interface: ansible_virtual_port_channel_interface + state: query + register: query_one + +- name: Query all Virtual Port Channel Interfaces in the template + cisco.mso.ndo_virtual_port_channel_interface: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_resource_template + state: query + register: query_all + +- name: Delete an Virtual Port Channel Interface + cisco.mso.ndo_virtual_port_channel_interface: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_resource_template + virtual_port_channel_interface: ansible_virtual_port_channel_interface + state: absent +""" + +RETURN = r""" +""" + +import copy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import ( + MSOModule, + mso_argument_spec, +) +from ansible_collections.cisco.mso.plugins.module_utils.template import ( + MSOTemplate, + KVPair, +) + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + template=dict(type="str", required=True), + virtual_port_channel_interface=dict(type="str", aliases=["name", "virtual_port_channel", "vpc"]), + virtual_port_channel_interface_uuid=dict(type="str", aliases=["uuid", "virtual_port_channel_uuid", "vpc_uuid"]), + description=dict(type="str"), + node_1=dict(type="str"), + node_2=dict(type="str"), + interfaces_1=dict(type="list", elements="str", aliases=["members_1"]), + interfaces_2=dict(type="list", elements="str", aliases=["members_2"]), + interface_policy_group=dict( + type="dict", + options=dict( + name=dict(type="str"), + template=dict(type="str"), + ), + aliases=["policy", "interface_policy"], + ), + interface_policy_group_uuid=dict(type="str", aliases=["policy_uuid", "interface_policy_uuid"]), + interface_descriptions=dict( + type="list", + elements="dict", + options=dict( + node=dict(type="str"), + interface_id=dict(type="str"), + description=dict(type="str"), + ), + ), + state=dict(type="str", default="query", choices=["absent", "query", "present"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["template", "virtual_port_channel_interface"]], + ["state", "present", ["template", "virtual_port_channel_interface"]], + ], + ) + + mso = MSOModule(module) + + template = module.params.get("template") + virtual_port_channel_interface = module.params.get("virtual_port_channel_interface") + virtual_port_channel_interface_uuid = module.params.get("virtual_port_channel_interface_uuid") + description = module.params.get("description") + node_1 = module.params.get("node_1") + node_2 = module.params.get("node_2") + interfaces_1 = module.params.get("interfaces_1") + if interfaces_1: + interfaces_1 = ",".join(interfaces_1) + interfaces_2 = module.params.get("interfaces_2") + if interfaces_2: + interfaces_2 = ",".join(interfaces_2) + interface_policy_group = module.params.get("interface_policy_group") + interface_policy_group_uuid = module.params.get("interface_policy_group_uuid") + interface_descriptions = module.params.get("interface_descriptions") + state = module.params.get("state") + + ops = [] + match = None + + mso_template = MSOTemplate(mso, "fabric_resource", template) + mso_template.validate_template("fabricResource") + object_description = "Virtual Port Channel Interface" + + path = "/fabricResourceTemplate/template/virtualPortChannels" + existing_virtual_port_channel_interfaces = mso_template.template.get("fabricResourceTemplate", {}).get("template", {}).get("virtualPortChannels", []) + if virtual_port_channel_interface or virtual_port_channel_interface_uuid: + match = mso_template.get_object_by_key_value_pairs( + object_description, + existing_virtual_port_channel_interfaces, + [KVPair("uuid", virtual_port_channel_interface_uuid) if virtual_port_channel_interface_uuid else KVPair("name", virtual_port_channel_interface)], + ) + if match: + mso.existing = mso.previous = copy.deepcopy(match.details) + else: + mso.existing = mso.previous = existing_virtual_port_channel_interfaces + + if state == "present": + + if interface_policy_group and not interface_policy_group_uuid: + fabric_policy_template = MSOTemplate(mso, "fabric_policy", interface_policy_group.get("template")) + fabric_policy_template.validate_template("fabricPolicy") + interface_policy_group_uuid = fabric_policy_template.get_interface_policy_group_uuid(interface_policy_group.get("name")) + + if match: + if virtual_port_channel_interface and match.details.get("name") != virtual_port_channel_interface: + ops.append(dict(op="replace", path="{0}/{1}/name".format(path, match.index), value=virtual_port_channel_interface)) + match.details["name"] = virtual_port_channel_interface + + if description and match.details.get("description") != description: + ops.append(dict(op="replace", path="{0}/{1}/description".format(path, match.index), value=description)) + match.details["description"] = description + + if node_1 and match.details.get("node1Details", {}).get("node") != node_1: + ops.append(dict(op="replace", path="{0}/{1}/node1Details/node".format(path, match.index), value=node_1)) + match.details["node1Details"]["node"] = node_1 + + if node_2 and match.details.get("node2Details", {}).get("node") != node_2: + ops.append(dict(op="replace", path="{0}/{1}/node2Details/node".format(path, match.index), value=node_2)) + match.details["node2Details"]["node"] = node_2 + + if interface_policy_group_uuid and match.details.get("policy") != interface_policy_group_uuid: + ops.append(dict(op="replace", path="{0}/{1}/policy".format(path, match.index), value=interface_policy_group_uuid)) + match.details["policy"] = interface_policy_group_uuid + + if interfaces_1 and interfaces_1 != match.details.get("node1Details", {}).get("memberInterfaces"): + ops.append(dict(op="replace", path="{0}/{1}/node1Details/memberInterfaces".format(path, match.index), value=interfaces_1)) + match.details["node1Details"]["memberInterfaces"] = interfaces_1 + + if interfaces_2 and interfaces_2 != match.details.get("node2Details", {}).get("memberInterfaces"): + ops.append(dict(op="replace", path="{0}/{1}/node2Details/memberInterfaces".format(path, match.index), value=interfaces_2)) + match.details["node2Details"]["memberInterfaces"] = interfaces_2 + + if interface_descriptions: + interface_descriptions = [ + { + "nodeID": interface.get("node"), + "interfaceID": interface.get("interface_id"), + "description": interface.get("description"), + } + for interface in interface_descriptions + ] + if interface_descriptions != match.details.get("interfaceDescriptions"): + ops.append(dict(op="replace", path="{0}/{1}/interfaceDescriptions".format(path, match.index), value=interface_descriptions)) + match.details["interfaceDescriptions"] = interface_descriptions + elif interface_descriptions == [] and match.details["interfaceDescriptions"]: + ops.append(dict(op="remove", path="{0}/{1}/interfaceDescriptions".format(path, match.index))) + + mso.sanitize(match.details) + + else: + payload = { + "name": virtual_port_channel_interface, + "node1Details": { + "node": node_1, + "memberInterfaces": interfaces_1, + }, + "node2Details": { + "node": node_2, + "memberInterfaces": interfaces_2, + }, + "policy": interface_policy_group_uuid, + } + if description: + payload["description"] = description + if interface_descriptions: + payload["interfaceDescriptions"] = [ + { + "nodeID": interface.get("node"), + "interfaceID": interface.get("interface_id"), + "description": interface.get("description"), + } + for interface in interface_descriptions + ] + + ops.append(dict(op="add", path="{0}/-".format(path), value=payload)) + + mso.sanitize(payload) + + elif state == "absent": + if match: + ops.append(dict(op="remove", path="{0}/{1}".format(path, match.index))) + + if not module.check_mode and ops: + response = mso.request(mso_template.template_path, method="PATCH", data=ops) + virtual_port_channel_interfaces = response.get("fabricResourceTemplate", {}).get("template", {}).get("virtualPortChannels", []) + match = mso_template.get_object_by_key_value_pairs( + object_description, + virtual_port_channel_interfaces, + [KVPair("uuid", virtual_port_channel_interface_uuid) if virtual_port_channel_interface_uuid else KVPair("name", virtual_port_channel_interface)], + ) + if match: + mso.existing = match.details + else: + mso.existing = {} + elif module.check_mode and state != "query": + mso.existing = mso.proposed if state == "present" else {} + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/ndo_virtual_port_channel_interface/aliases b/tests/integration/targets/ndo_virtual_port_channel_interface/aliases new file mode 100644 index 00000000..5042c9c0 --- /dev/null +++ b/tests/integration/targets/ndo_virtual_port_channel_interface/aliases @@ -0,0 +1,2 @@ +# No ACI MultiSite infrastructure, so not enabled +# unsupported diff --git a/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml b/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml new file mode 100644 index 00000000..6fa14281 --- /dev/null +++ b/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml @@ -0,0 +1,401 @@ +# Test code for the MSO modules +# Copyright: (c) 2024, Gaspard Micol (@gmicol) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +- name: Test that we have an ACI MultiSite host, username and password + ansible.builtin.fail: + msg: 'Please define the following variables: mso_hostname, mso_username and mso_password.' + when: mso_hostname is not defined or mso_username is not defined or mso_password is not defined + +# CLEAN ENVIRONMENT +- name: Set vars + ansible.builtin.set_fact: + mso_info: &mso_info + host: '{{ mso_hostname }}' + username: '{{ mso_username }}' + password: '{{ mso_password }}' + validate_certs: '{{ mso_validate_certs | default(false) }}' + use_ssl: '{{ mso_use_ssl | default(true) }}' + use_proxy: '{{ mso_use_proxy | default(true) }}' + output_level: '{{ mso_output_level | default("info") }}' + +# QUERY VERSION +- name: Query MSO version + cisco.mso.mso_version: + <<: *mso_info + state: query + register: version + + +- name: Execute tasks only for MSO version > 4.4 + when: version.current.version is version('4.4', '>=') + block: + + - name: Ensure sites exists + cisco.mso.mso_site: + <<: *mso_info + site: '{{ item.site }}' + apic_username: '{{ apic_username }}' + apic_password: '{{ apic_password }}' + apic_site_id: '{{ item.apic_site_id }}' + urls: + - https://{{ apic_hostname }} + state: present + loop: + - {site: "ansible_test", apic_site_id: 101} + - {site: "ansible_test_2", apic_site_id: 102} + + - name: Ensure fabric resource template does not exist + cisco.mso.ndo_template: &template_absent + <<: *mso_info + name: ansible_fabric_resource_template + template_type: fabric_resource + state: absent + + - name: Create fabric resource template + cisco.mso.ndo_template: + <<: *template_absent + state: present + + # CREATE + + - name: Create a new virtual port channel interface (check_mode) + cisco.mso.ndo_virtual_port_channel_interface: &create_virtual_port_channel_interface + <<: *mso_info + template: ansible_fabric_resource_template + virtual_port_channel_interface: ansible_virtual_port_channel_interface + description: Ansible Virtual Port Channel test + node_1: 101 + node_2: 102 + interfaces_1: 1/1 + interfaces_2: 1/1 + interface_policy_group: + name: Gaspard_Interface_setting_test + template: Gaspard_FP_3.2_test + interface_descriptions: + - node: 101 + interface_id: 1/1 + description: first Ansible interface test for first node + - node: 102 + interface_id: 1/1 + description: first Ansible interface test for second node + state: present + check_mode: true + register: cm_create_new_virtual_port_channel_interface + + - name: Create a new virtual port channel interface + cisco.mso.ndo_virtual_port_channel_interface: + <<: *create_virtual_port_channel_interface + register: nm_create_new_virtual_port_channel_interface + + - name: Create a new virtual port channel interface again + cisco.mso.ndo_virtual_port_channel_interface: + <<: *create_virtual_port_channel_interface + register: nm_create_new_virtual_port_channel_interface_again + + - name: Assert virtual port channel interface creation tasks + assert: + that: + - cm_create_new_virtual_port_channel_interface is changed + - cm_create_new_virtual_port_channel_interface.previous == {} + - cm_create_new_virtual_port_channel_interface.current.name == "ansible_virtual_port_channel_interface" + - cm_create_new_virtual_port_channel_interface.current.description == "Ansible Virtual Port Channel test" + - cm_create_new_virtual_port_channel_interface.current.node1Details.node == "101" + - cm_create_new_virtual_port_channel_interface.current.node1Details.memberInterfaces == "1/1" + - cm_create_new_virtual_port_channel_interface.current.node2Details.node == "102" + - cm_create_new_virtual_port_channel_interface.current.node2Details.memberInterfaces == "1/1" + - cm_create_new_virtual_port_channel_interface.current.interfaceDescriptions | length == 2 + - cm_create_new_virtual_port_channel_interface.current.interfaceDescriptions.0.nodeID == "101" + - cm_create_new_virtual_port_channel_interface.current.interfaceDescriptions.0.interfaceID == "1/1" + - cm_create_new_virtual_port_channel_interface.current.interfaceDescriptions.0.description == "first Ansible interface test for first node" + - cm_create_new_virtual_port_channel_interface.current.interfaceDescriptions.1.nodeID == "102" + - cm_create_new_virtual_port_channel_interface.current.interfaceDescriptions.1.interfaceID == "1/1" + - cm_create_new_virtual_port_channel_interface.current.interfaceDescriptions.1.description == "first Ansible interface test for second node" + - nm_create_new_virtual_port_channel_interface is changed + - nm_create_new_virtual_port_channel_interface.previous == {} + - nm_create_new_virtual_port_channel_interface.current.name == "ansible_virtual_port_channel_interface" + - nm_create_new_virtual_port_channel_interface.current.description == "Ansible Virtual Port Channel test" + - nm_create_new_virtual_port_channel_interface.current.node1Details.node == "101" + - nm_create_new_virtual_port_channel_interface.current.node1Details.memberInterfaces == "1/1" + - nm_create_new_virtual_port_channel_interface.current.node2Details.node == "102" + - nm_create_new_virtual_port_channel_interface.current.node2Details.memberInterfaces == "1/1" + - nm_create_new_virtual_port_channel_interface.current.interfaceDescriptions | length == 2 + - nm_create_new_virtual_port_channel_interface.current.interfaceDescriptions.0.nodeID == "101" + - nm_create_new_virtual_port_channel_interface.current.interfaceDescriptions.0.interfaceID == "1/1" + - nm_create_new_virtual_port_channel_interface.current.interfaceDescriptions.0.description == "first Ansible interface test for first node" + - nm_create_new_virtual_port_channel_interface.current.interfaceDescriptions.1.nodeID == "102" + - nm_create_new_virtual_port_channel_interface.current.interfaceDescriptions.1.interfaceID == "1/1" + - nm_create_new_virtual_port_channel_interface.current.interfaceDescriptions.1.description == "first Ansible interface test for second node" + - nm_create_new_virtual_port_channel_interface_again is not changed + - nm_create_new_virtual_port_channel_interface_again.previous == nm_create_new_virtual_port_channel_interface_again.current + - nm_create_new_virtual_port_channel_interface_again.current.name == "ansible_virtual_port_channel_interface" + - nm_create_new_virtual_port_channel_interface_again.current.description == "Ansible Virtual Port Channel test" + - nm_create_new_virtual_port_channel_interface_again.current.node1Details.node == "101" + - nm_create_new_virtual_port_channel_interface_again.current.node1Details.memberInterfaces == "1/1" + - nm_create_new_virtual_port_channel_interface_again.current.node2Details.node == "102" + - nm_create_new_virtual_port_channel_interface_again.current.node2Details.memberInterfaces == "1/1" + - nm_create_new_virtual_port_channel_interface_again.current.interfaceDescriptions | length == 2 + - nm_create_new_virtual_port_channel_interface_again.current.interfaceDescriptions.0.nodeID == "101" + - nm_create_new_virtual_port_channel_interface_again.current.interfaceDescriptions.0.interfaceID == "1/1" + - nm_create_new_virtual_port_channel_interface_again.current.interfaceDescriptions.0.description == "first Ansible interface test for first node" + - nm_create_new_virtual_port_channel_interface_again.current.interfaceDescriptions.1.nodeID == "102" + - nm_create_new_virtual_port_channel_interface_again.current.interfaceDescriptions.1.interfaceID == "1/1" + - nm_create_new_virtual_port_channel_interface_again.current.interfaceDescriptions.1.description == "first Ansible interface test for second node" + + # UPDATE + + - name: Update a virtual port channel interface first node (check_mode) + cisco.mso.ndo_virtual_port_channel_interface: &update_virtual_port_channel_interface + <<: *create_virtual_port_channel_interface + node_1: 103 + interface_descriptions: + - node: 103 + interface_id: 1/1 + description: first Ansible interface test for first node + - node: 102 + interface_id: 1/1 + description: first Ansible interface test for second node + check_mode: true + register: cm_update_virtual_port_channel_interface_first_node + + - name: Update a virtual port channel interface first node + cisco.mso.ndo_virtual_port_channel_interface: + <<: *update_virtual_port_channel_interface + register: nm_update_virtual_port_channel_interface_first_node + + - name: Update a virtual port channel interface first node again + cisco.mso.ndo_virtual_port_channel_interface: + <<: *update_virtual_port_channel_interface + register: nm_update_virtual_port_channel_interface_first_node_again + + - name: Update a virtual port channel interface second node + cisco.mso.ndo_virtual_port_channel_interface: &update_virtual_port_channel_interface_second_node + <<: *update_virtual_port_channel_interface + node_2: 104 + interface_descriptions: + - node: 103 + interface_id: 1/1 + description: first Ansible interface test for first node + - node: 104 + interface_id: 1/1 + description: first Ansible interface test for second node + state: present + register: nm_update_virtual_port_channel_interface_second_node + + - name: Update a virtual port channel interface name + cisco.mso.ndo_virtual_port_channel_interface: &update_virtual_port_channel_interface_name + <<: *update_virtual_port_channel_interface_second_node + virtual_port_channel_interface_uuid: '{{ nm_update_virtual_port_channel_interface_second_node.current.uuid }}' + virtual_port_channel_interface: ansible_virtual_port_channel_interface_changed + register: nm_update_virtual_port_channel_interface_name + + - name: Update a virtual port channel interface description + cisco.mso.ndo_virtual_port_channel_interface: &update_virtual_port_channel_interface_description + <<: *update_virtual_port_channel_interface_name + description: Ansible Virtual Port Channel test updated + register: nm_update_virtual_port_channel_interface_description + + - name: Update a virtual port channel interface policy + cisco.mso.ndo_virtual_port_channel_interface: &update_virtual_port_channel_interface_policy_group + <<: *update_virtual_port_channel_interface_description + interface_policy_group: + name: Gaspard_Interface_setting_test_2 + template: Gaspard_FP_3.2_test + register: nm_update_virtual_port_channel_interface_policy_group + + - name: Update a virtual port channel interface members for first node + cisco.mso.ndo_virtual_port_channel_interface: &update_virtual_port_channel_first_interface_members + <<: *update_virtual_port_channel_interface_policy_group + interfaces_1: 1/1-3 + register: nm_update_virtual_port_channel_first_interface_members + + - name: Update a virtual port channel interface members for second node + cisco.mso.ndo_virtual_port_channel_interface: &update_virtual_port_channel_second_interface_members + <<: *update_virtual_port_channel_first_interface_members + interfaces_2: 1/1-2 + register: nm_update_virtual_port_channel_second_interface_members + + - name: Update a virtual port channel interface members descriptions + cisco.mso.ndo_virtual_port_channel_interface: &update_virtual_port_channel_interface_descriptions + <<: *update_virtual_port_channel_second_interface_members + interface_descriptions: + - node: 103 + interface_id: 1/1 + description: new first Ansible interface test for first node + - node: 103 + interface_id: 1/2 + description: new second Ansible interface test for first node + - node: 103 + interface_id: 1/3 + description: new third Ansible interface test for first node + - node: 104 + interface_id: 1/1 + description: new first Ansible interface test for second node + - node: 104 + interface_id: 1/2 + description: new second Ansible interface test for second node + register: nm_update_virtual_port_channel_interface_descriptions + + - name: Delete a virtual port channel interface members descriptions + cisco.mso.ndo_virtual_port_channel_interface: &delete_virtual_port_channel_interface_desciptions + <<: *update_virtual_port_channel_interface_descriptions + interface_descriptions: [] + register: nm_delete_virtual_port_channel_interface_descriptions + + - name: Delete a virtual port channel interface member and add descriptions again for first node + cisco.mso.ndo_virtual_port_channel_interface: &delete_virtual_port_channel_interface_member + <<: *delete_virtual_port_channel_interface_desciptions + interfaces_1: 1/1-2 + interface_descriptions: + - node: 103 + interface_id: 1/1 + description: new first Ansible interface test for first node + - node: 103 + interface_id: 1/2 + description: new second Ansible interface test for first node + register: nm_delete_virtual_port_channel_interface_member + + - name: Assert virtual port channel interface update tasks + assert: + that: + - cm_update_virtual_port_channel_interface_first_node is changed + - cm_update_virtual_port_channel_interface_first_node.current.node1Details.node == "103" + - cm_update_virtual_port_channel_interface_first_node.current.interfaceDescriptions | length == 2 + - cm_update_virtual_port_channel_interface_first_node.current.interfaceDescriptions.0.nodeID == "103" + - cm_update_virtual_port_channel_interface_first_node.current.interfaceDescriptions.0.interfaceID == "1/1" + - cm_update_virtual_port_channel_interface_first_node.current.interfaceDescriptions.0.description == "first Ansible interface test for first node" + - cm_update_virtual_port_channel_interface_first_node.current.interfaceDescriptions.1.nodeID == "102" + - cm_update_virtual_port_channel_interface_first_node.current.interfaceDescriptions.1.interfaceID == "1/1" + - cm_update_virtual_port_channel_interface_first_node.current.interfaceDescriptions.1.description == "first Ansible interface test for second node" + - nm_update_virtual_port_channel_interface_first_node is changed + - nm_update_virtual_port_channel_interface_first_node.current.node1Details.node == "103" + - nm_update_virtual_port_channel_interface_first_node.current.interfaceDescriptions | length == 2 + - nm_update_virtual_port_channel_interface_first_node.current.interfaceDescriptions.0.nodeID == "103" + - nm_update_virtual_port_channel_interface_first_node.current.interfaceDescriptions.0.interfaceID == "1/1" + - nm_update_virtual_port_channel_interface_first_node.current.interfaceDescriptions.0.description == "first Ansible interface test for first node" + - nm_update_virtual_port_channel_interface_first_node.current.interfaceDescriptions.1.nodeID == "102" + - nm_update_virtual_port_channel_interface_first_node.current.interfaceDescriptions.1.interfaceID == "1/1" + - nm_update_virtual_port_channel_interface_first_node.current.interfaceDescriptions.1.description == "first Ansible interface test for second node" + - nm_update_virtual_port_channel_interface_first_node_again is not changed + - nm_update_virtual_port_channel_interface_first_node_again.current.interfaceDescriptions.0.nodeID == "103" + - nm_update_virtual_port_channel_interface_first_node_again.current == nm_update_virtual_port_channel_interface_first_node_again.previous + - nm_update_virtual_port_channel_interface_second_node is changed + - nm_update_virtual_port_channel_interface_second_node.current.node2Details.node == "104" + - nm_update_virtual_port_channel_interface_second_node.current.interfaceDescriptions | length == 2 + - nm_update_virtual_port_channel_interface_second_node.current.interfaceDescriptions.0.nodeID == "103" + - nm_update_virtual_port_channel_interface_second_node.current.interfaceDescriptions.0.interfaceID == "1/1" + - nm_update_virtual_port_channel_interface_second_node.current.interfaceDescriptions.0.description == "first Ansible interface test for first node" + - nm_update_virtual_port_channel_interface_second_node.current.interfaceDescriptions.1.nodeID == "104" + - nm_update_virtual_port_channel_interface_second_node.current.interfaceDescriptions.1.interfaceID == "1/1" + - nm_update_virtual_port_channel_interface_second_node.current.interfaceDescriptions.1.description == "first Ansible interface test for second node" + - nm_update_virtual_port_channel_interface_name is changed + - nm_update_virtual_port_channel_interface_name.current.name == "ansible_virtual_port_channel_interface_changed" + - nm_update_virtual_port_channel_interface_description is changed + - nm_update_virtual_port_channel_interface_description.current.description == "Ansible Virtual Port Channel test updated" + - nm_update_virtual_port_channel_first_interface_members is changed + - nm_update_virtual_port_channel_first_interface_members.current.node1Details.memberInterfaces == "1/1-3" + - nm_update_virtual_port_channel_second_interface_members is changed + - nm_update_virtual_port_channel_second_interface_members.current.node1Details.memberInterfaces == "1/1-2" + - nm_update_virtual_port_channel_interface_descriptions is changed + - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions | length == 5 + - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions.0.nodeID == "103" + - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions.0.interfaceID == "1/1" + - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions.0.description == "new first Ansible interface test for first node" + - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions.1.nodeID == "103" + - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions.1.interfaceID == "1/2" + - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions.1.description == "new second Ansible interface test for first node" + - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions.2.nodeID == "103" + - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions.2.interfaceID == "1/3" + - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions.2.description == "new third Ansible interface test for first node" + - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions.3.nodeID == "104" + - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions.3.interfaceID == "1/1" + - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions.3.description == "new first Ansible interface test for second node" + - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions.4.nodeID == "104" + - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions.4.interfaceID == "1/2" + - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions.4.description == "new second Ansible interface test for second node" + - nm_delete_virtual_port_channel_interface_descriptions is changed + - nm_delete_virtual_port_channel_interface_member is changed + - nm_delete_virtual_port_channel_interface_member.current.node1Details.memberInterfaces == "1/1-2" + - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions | length == 2 + - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions.0.nodeID == "103" + - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions.0.interfaceID == "1/1" + - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions.0.description == "new first Ansible interface test for first node" + - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions.1.nodeID == "103" + - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions.1.interfaceID == "1/2" + - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions.1.description == "new second Ansible interface test for first node" + + # QUERY + + - name: Create another virtual port channel interface + cisco.mso.ndo_virtual_port_channel_interface: &create_virtual_port_channel_interface_2 + <<: *mso_info + template: ansible_fabric_resource_template + virtual_port_channel_interface: ansible_virtual_port_channel_interface_2 + node_1: 101 + node_2: 102 + interfaces_1: 1/1 + interfaces_2: 1/1 + interface_policy_group: + name: Gaspard_Interface_setting_test + template: Gaspard_FP_3.2_test + state: present + + - name: Query a virtual port channel interface with template_name + cisco.mso.ndo_virtual_port_channel_interface: + <<: *create_virtual_port_channel_interface_2 + state: query + register: query_one + + - name: Query all virtual port channel interfaces in the template + cisco.mso.ndo_virtual_port_channel_interface: + <<: *mso_info + template: ansible_fabric_resource_template + state: query + register: query_all + + - name: Assert virtual port channel interface query tasks + assert: + that: + - query_one is not changed + - query_one.current.name == "ansible_virtual_port_channel_interface_2" + - query_all is not changed + - query_all.current | length == 2 + + # DELETE + + - name: Delete a virtual port channel interface (check_mode) + cisco.mso.ndo_virtual_port_channel_interface: &delete_virtual_port_channel_interface + <<: *delete_virtual_port_channel_interface_member + state: absent + check_mode: true + register: cm_delete_virtual_port_channel_interface + + - name: Delete a virtual port channel interface + cisco.mso.ndo_virtual_port_channel_interface: + <<: *delete_virtual_port_channel_interface + register: nm_delete_virtual_port_channel_interface + + - name: Delete a virtual port channel interface again + cisco.mso.ndo_virtual_port_channel_interface: + <<: *delete_virtual_port_channel_interface + register: nm_delete_virtual_port_channel_interface_again + + - name: Assert virtual port channel interface deletion tasks + assert: + that: + - cm_delete_virtual_port_channel_interface is changed + - cm_delete_virtual_port_channel_interface.previous.name == "ansible_virtual_port_channel_interface_changed" + - cm_delete_virtual_port_channel_interface.current == {} + - nm_delete_virtual_port_channel_interface is changed + - nm_delete_virtual_port_channel_interface.previous.name == "ansible_virtual_port_channel_interface_changed" + - nm_delete_virtual_port_channel_interface.current == {} + - nm_delete_virtual_port_channel_interface_again is not changed + - nm_delete_virtual_port_channel_interface_again.previous == {} + - nm_delete_virtual_port_channel_interface_again.current == {} + + # CLEANUP TEMPLATE + + - name: Ensure templates do not exist + cisco.mso.ndo_template: + <<: *template_absent From eb959d3846deb467c582fe6af56b4abbac43ccfd Mon Sep 17 00:00:00 2001 From: Gaspard Micol Date: Tue, 29 Oct 2024 13:26:58 -0400 Subject: [PATCH 02/11] [ignore] Modify Documentations and interfaces attribute's name. --- .../ndo_virtual_port_channel_interface.py | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/plugins/modules/ndo_virtual_port_channel_interface.py b/plugins/modules/ndo_virtual_port_channel_interface.py index 82c19002..4f84f106 100644 --- a/plugins/modules/ndo_virtual_port_channel_interface.py +++ b/plugins/modules/ndo_virtual_port_channel_interface.py @@ -18,9 +18,9 @@ DOCUMENTATION = r""" --- module: ndo_virtual_port_channel_interface -short_description: Manage Virtual Port Channel Interfaces_1 on Cisco Nexus Dashboard Orchestrator (NDO). +short_description: Manage Virtual Port Channel Interfaces on Cisco Nexus Dashboard Orchestrator (NDO). description: -- Manage Virtual Port Channel Interfaces_1 on Cisco Nexus Dashboard Orchestrator (NDO). +- Manage Virtual Port Channel Interfaces on Cisco Nexus Dashboard Orchestrator (NDO). - This module is only supported on ND v3.2 (NDO v4.4) and later. author: - Gaspard Micol (@gmicol) @@ -38,13 +38,13 @@ aliases: [ name, virtual_port_channel, vpc ] virtual_port_channel_interface_uuid: description: - - The uuid of the Virtual Port Channel Interface. + - The UUID of the Virtual Port Channel Interface. - This parameter is required when parameter O(virtual_port_channel_interface) is updated. type: str aliases: [ uuid, virtual_port_channel_uuid, vpc_uuid ] description: description: - - The description of the Port Channel Interface. + - The description of the Virtual Port Channel Interface. type: str node_1: description: @@ -54,20 +54,20 @@ description: - The second node ID. type: str - interfaces_1: + interfaces_node_1: description: - The list of used Interface IDs for the first node. - Ranges of Interface IDs can be used. type: list elements: str - aliases: [ members_1 ] - interfaces_2: + aliases: [ interfaces_1, members_1 ] + interfaces_node_2: description: - The list of used Interface IDs for the second node. - Ranges of Interface IDs can be used. type: list elements: str - aliases: [ members_2 ] + aliases: [ interfaces_2, members_2 ] interface_policy_group_uuid: description: - The UUID of the Port Channel Interface Setting Policy. @@ -96,7 +96,7 @@ suboptions: node: description: - - The node ID + - The node ID. type: str interface_id: description: @@ -128,10 +128,10 @@ virtual_port_channel_interface: ansible_virtual_port_channel_interface node_1: 101 node_2: 102 - interfaces_1: + interfaces_node_1: - 1/1 - 1/10-11 - interfaces_2: + interfaces_node_2: - 1/2 interface_policy_group: name: ansible_policy_group @@ -204,8 +204,8 @@ def main(): description=dict(type="str"), node_1=dict(type="str"), node_2=dict(type="str"), - interfaces_1=dict(type="list", elements="str", aliases=["members_1"]), - interfaces_2=dict(type="list", elements="str", aliases=["members_2"]), + interfaces_node_1=dict(type="list", elements="str", aliases=["interfaces_1", "members_1"]), + interfaces_node_2=dict(type="list", elements="str", aliases=["interfaces_2", "members_2"]), interface_policy_group=dict( type="dict", options=dict( @@ -244,12 +244,12 @@ def main(): description = module.params.get("description") node_1 = module.params.get("node_1") node_2 = module.params.get("node_2") - interfaces_1 = module.params.get("interfaces_1") - if interfaces_1: - interfaces_1 = ",".join(interfaces_1) - interfaces_2 = module.params.get("interfaces_2") - if interfaces_2: - interfaces_2 = ",".join(interfaces_2) + interfaces_node_1 = module.params.get("interfaces_node_1") + if interfaces_node_1: + interfaces_node_1 = ",".join(interfaces_node_1) + interfaces_node_2 = module.params.get("interfaces_node_2") + if interfaces_node_2: + interfaces_node_2 = ",".join(interfaces_node_2) interface_policy_group = module.params.get("interface_policy_group") interface_policy_group_uuid = module.params.get("interface_policy_group_uuid") interface_descriptions = module.params.get("interface_descriptions") @@ -303,13 +303,13 @@ def main(): ops.append(dict(op="replace", path="{0}/{1}/policy".format(path, match.index), value=interface_policy_group_uuid)) match.details["policy"] = interface_policy_group_uuid - if interfaces_1 and interfaces_1 != match.details.get("node1Details", {}).get("memberInterfaces"): - ops.append(dict(op="replace", path="{0}/{1}/node1Details/memberInterfaces".format(path, match.index), value=interfaces_1)) - match.details["node1Details"]["memberInterfaces"] = interfaces_1 + if interfaces_node_1 and interfaces_node_1 != match.details.get("node1Details", {}).get("memberInterfaces"): + ops.append(dict(op="replace", path="{0}/{1}/node1Details/memberInterfaces".format(path, match.index), value=interfaces_node_1)) + match.details["node1Details"]["memberInterfaces"] = interfaces_node_1 - if interfaces_2 and interfaces_2 != match.details.get("node2Details", {}).get("memberInterfaces"): - ops.append(dict(op="replace", path="{0}/{1}/node2Details/memberInterfaces".format(path, match.index), value=interfaces_2)) - match.details["node2Details"]["memberInterfaces"] = interfaces_2 + if interfaces_node_2 and interfaces_node_2 != match.details.get("node2Details", {}).get("memberInterfaces"): + ops.append(dict(op="replace", path="{0}/{1}/node2Details/memberInterfaces".format(path, match.index), value=interfaces_node_2)) + match.details["node2Details"]["memberInterfaces"] = interfaces_node_2 if interface_descriptions: interface_descriptions = [ @@ -333,11 +333,11 @@ def main(): "name": virtual_port_channel_interface, "node1Details": { "node": node_1, - "memberInterfaces": interfaces_1, + "memberInterfaces": interfaces_node_1, }, "node2Details": { "node": node_2, - "memberInterfaces": interfaces_2, + "memberInterfaces": interfaces_node_2, }, "policy": interface_policy_group_uuid, } From cd1f639214a0b180aa66be192d1c77fe73a3ae1e Mon Sep 17 00:00:00 2001 From: Gaspard Micol Date: Wed, 30 Oct 2024 14:01:47 -0400 Subject: [PATCH 03/11] [ignore] Modify Documentations and Add more examples. Modify Logic for possible empty strings attributes. --- .../ndo_virtual_port_channel_interface.py | 71 ++++++++++++++----- 1 file changed, 53 insertions(+), 18 deletions(-) diff --git a/plugins/modules/ndo_virtual_port_channel_interface.py b/plugins/modules/ndo_virtual_port_channel_interface.py index 4f84f106..3bf38836 100644 --- a/plugins/modules/ndo_virtual_port_channel_interface.py +++ b/plugins/modules/ndo_virtual_port_channel_interface.py @@ -39,6 +39,8 @@ virtual_port_channel_interface_uuid: description: - The UUID of the Virtual Port Channel Interface. + - This parameter can be used instead of O(virtual_port_channel_interface) + when an existing Virtual Port Channel Interface is updated. - This parameter is required when parameter O(virtual_port_channel_interface) is updated. type: str aliases: [ uuid, virtual_port_channel_uuid, vpc_uuid ] @@ -58,6 +60,7 @@ description: - The list of used Interface IDs for the first node. - Ranges of Interface IDs can be used. + - This parameter is required during creation. type: list elements: str aliases: [ interfaces_1, members_1 ] @@ -65,32 +68,35 @@ description: - The list of used Interface IDs for the second node. - Ranges of Interface IDs can be used. + - This parameter is required during creation. type: list elements: str aliases: [ interfaces_2, members_2 ] interface_policy_group_uuid: description: - - The UUID of the Port Channel Interface Setting Policy. + - The UUID of the Port Channel Interface Policy Group. + - An Port Channel Interface Policy Group must be attached required during creation. type: str - aliases: [ policy_uuid, interface_policy_uuid ] + aliases: [ policy_uuid, interface_policy_uuid, interface_setting_uuid ] interface_policy_group: description: - The name of the Port Channel Interface Policy Group. - This parameter can be used instead of O(interface_policy_group_uuid). + - If both parameter are used, O(interface_policy_group) will be ignored. type: dict suboptions: name: description: - - The name of the Port Channel Interface Setting Policy. + - The name of the Port Channel Interface Policy Group. type: str template: description: - - The name of the template in which is referred the Port Channel Interface Policy Group. + - The name of the template in which the Port Channel Interface Policy Group has been created. type: str - aliases: [ policy, interface_policy ] + aliases: [ policy, interface_policy, interface_setting ] interface_descriptions: description: - - The list of descriptions for each interface. + - The list of interface descriptions. type: list elements: dict suboptions: @@ -151,16 +157,36 @@ description: My first Ansible Interface for second node state: present -- name: Query an Virtual Port Channel Interface with template_name +- name: Update Virtual Port Channel Interface's name cisco.mso.ndo_virtual_port_channel_interface: host: mso_host username: admin password: SomeSecretPassword template: ansible_fabric_resource_template - virtual_port_channel_interface: ansible_virtual_port_channel_interface + virtual_port_channel_interface: ansible_virtual_port_channel_interface_changed + virtual_port_channel_interface_uuid: 0134c73f-4427-4109-9eea-5110ecdf10ea + state: present + +- name: Query an Virtual Port Channel Interface using it's name in the template + cisco.mso.ndo_virtual_port_channel_interface: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_resource_template + virtual_port_channel_interface: ansible_virtual_port_channel_interface_changed state: query register: query_one +- name: Query an Virtual Port Channel Interface using it's uuid in the template + cisco.mso.ndo_virtual_port_channel_interface: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_resource_template + virtual_port_channel_interface_uuid: 0134c73f-4427-4109-9eea-5110ecdf10ea + state: query + register: query_one_uuid + - name: Query all Virtual Port Channel Interfaces in the template cisco.mso.ndo_virtual_port_channel_interface: host: mso_host @@ -170,13 +196,22 @@ state: query register: query_all -- name: Delete an Virtual Port Channel Interface +- name: Delete an Virtual Port Channel Interface using it's name cisco.mso.ndo_virtual_port_channel_interface: host: mso_host username: admin password: SomeSecretPassword template: ansible_fabric_resource_template - virtual_port_channel_interface: ansible_virtual_port_channel_interface + virtual_port_channel_interface: ansible_virtual_port_channel_interface_changed + state: absent + +- name: Delete an Virtual Port Channel Interface using it's uuid + cisco.mso.ndo_virtual_port_channel_interface: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_resource_template + virtual_port_channel_interface_uuid: 0134c73f-4427-4109-9eea-5110ecdf10ea state: absent """ @@ -212,9 +247,9 @@ def main(): name=dict(type="str"), template=dict(type="str"), ), - aliases=["policy", "interface_policy"], + aliases=["policy", "interface_policy", "interface_setting"], ), - interface_policy_group_uuid=dict(type="str", aliases=["policy_uuid", "interface_policy_uuid"]), + interface_policy_group_uuid=dict(type="str", aliases=["policy_uuid", "interface_policy_uuid", "interface_setting_uuid"]), interface_descriptions=dict( type="list", elements="dict", @@ -231,8 +266,8 @@ def main(): argument_spec=argument_spec, supports_check_mode=True, required_if=[ - ["state", "absent", ["template", "virtual_port_channel_interface"]], - ["state", "present", ["template", "virtual_port_channel_interface"]], + ["state", "absent", ["virtual_port_channel_interface", "virtual_port_channel_interface_uuid"], True], + ["state", "present", ["virtual_port_channel_interface", "virtual_port_channel_interface_uuid"], True], ], ) @@ -287,15 +322,15 @@ def main(): ops.append(dict(op="replace", path="{0}/{1}/name".format(path, match.index), value=virtual_port_channel_interface)) match.details["name"] = virtual_port_channel_interface - if description and match.details.get("description") != description: + if description is not None and match.details.get("description") != description: ops.append(dict(op="replace", path="{0}/{1}/description".format(path, match.index), value=description)) match.details["description"] = description - if node_1 and match.details.get("node1Details", {}).get("node") != node_1: + if node_1 is not None and match.details.get("node1Details", {}).get("node") != node_1: ops.append(dict(op="replace", path="{0}/{1}/node1Details/node".format(path, match.index), value=node_1)) match.details["node1Details"]["node"] = node_1 - if node_2 and match.details.get("node2Details", {}).get("node") != node_2: + if node_2 is not None and match.details.get("node2Details", {}).get("node") != node_2: ops.append(dict(op="replace", path="{0}/{1}/node2Details/node".format(path, match.index), value=node_2)) match.details["node2Details"]["node"] = node_2 @@ -341,7 +376,7 @@ def main(): }, "policy": interface_policy_group_uuid, } - if description: + if description is not None: payload["description"] = description if interface_descriptions: payload["interfaceDescriptions"] = [ From 40893d800955028d24a67b1f8b569d4c98078f54 Mon Sep 17 00:00:00 2001 From: Gaspard Micol Date: Thu, 31 Oct 2024 13:53:10 -0400 Subject: [PATCH 04/11] [ignore] Add function in module_utils/mso.py to format interface_descriptions for payload data. --- .../ndo_virtual_port_channel_interface.py | 35 ++++++------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/plugins/modules/ndo_virtual_port_channel_interface.py b/plugins/modules/ndo_virtual_port_channel_interface.py index 3bf38836..eadb381b 100644 --- a/plugins/modules/ndo_virtual_port_channel_interface.py +++ b/plugins/modules/ndo_virtual_port_channel_interface.py @@ -60,7 +60,7 @@ description: - The list of used Interface IDs for the first node. - Ranges of Interface IDs can be used. - - This parameter is required during creation. + - This parameter is required when creating a new Virtual Port Channel Interface. type: list elements: str aliases: [ interfaces_1, members_1 ] @@ -68,14 +68,14 @@ description: - The list of used Interface IDs for the second node. - Ranges of Interface IDs can be used. - - This parameter is required during creation. + - This parameter is required when creating a new Virtual Port Channel Interface. type: list elements: str aliases: [ interfaces_2, members_2 ] interface_policy_group_uuid: description: - The UUID of the Port Channel Interface Policy Group. - - An Port Channel Interface Policy Group must be attached required during creation. + - This parameter is required when creating a new Virtual Port Channel Interface. type: str aliases: [ policy_uuid, interface_policy_uuid, interface_setting_uuid ] interface_policy_group: @@ -157,7 +157,7 @@ description: My first Ansible Interface for second node state: present -- name: Update Virtual Port Channel Interface's name +- name: Update a Virtual Port Channel Interface's name cisco.mso.ndo_virtual_port_channel_interface: host: mso_host username: admin @@ -167,7 +167,7 @@ virtual_port_channel_interface_uuid: 0134c73f-4427-4109-9eea-5110ecdf10ea state: present -- name: Query an Virtual Port Channel Interface using it's name in the template +- name: Query a Virtual Port Channel Interface using its name in the template cisco.mso.ndo_virtual_port_channel_interface: host: mso_host username: admin @@ -177,7 +177,7 @@ state: query register: query_one -- name: Query an Virtual Port Channel Interface using it's uuid in the template +- name: Query a Virtual Port Channel Interface using its UUID in the template cisco.mso.ndo_virtual_port_channel_interface: host: mso_host username: admin @@ -196,7 +196,7 @@ state: query register: query_all -- name: Delete an Virtual Port Channel Interface using it's name +- name: Delete a Virtual Port Channel Interface using its name cisco.mso.ndo_virtual_port_channel_interface: host: mso_host username: admin @@ -205,7 +205,7 @@ virtual_port_channel_interface: ansible_virtual_port_channel_interface_changed state: absent -- name: Delete an Virtual Port Channel Interface using it's uuid +- name: Delete a Virtual Port Channel Interface using its UUID cisco.mso.ndo_virtual_port_channel_interface: host: mso_host username: admin @@ -223,6 +223,7 @@ from ansible_collections.cisco.mso.plugins.module_utils.mso import ( MSOModule, mso_argument_spec, + format_interface_descriptions, ) from ansible_collections.cisco.mso.plugins.module_utils.template import ( MSOTemplate, @@ -347,14 +348,7 @@ def main(): match.details["node2Details"]["memberInterfaces"] = interfaces_node_2 if interface_descriptions: - interface_descriptions = [ - { - "nodeID": interface.get("node"), - "interfaceID": interface.get("interface_id"), - "description": interface.get("description"), - } - for interface in interface_descriptions - ] + interface_descriptions = format_interface_descriptions(interface_descriptions) if interface_descriptions != match.details.get("interfaceDescriptions"): ops.append(dict(op="replace", path="{0}/{1}/interfaceDescriptions".format(path, match.index), value=interface_descriptions)) match.details["interfaceDescriptions"] = interface_descriptions @@ -379,14 +373,7 @@ def main(): if description is not None: payload["description"] = description if interface_descriptions: - payload["interfaceDescriptions"] = [ - { - "nodeID": interface.get("node"), - "interfaceID": interface.get("interface_id"), - "description": interface.get("description"), - } - for interface in interface_descriptions - ] + payload["interfaceDescriptions"] = format_interface_descriptions(interface_descriptions) ops.append(dict(op="add", path="{0}/-".format(path), value=payload)) From 7ce7970aca764e2891928678b9ca38614844d371 Mon Sep 17 00:00:00 2001 From: Gaspard Micol Date: Fri, 8 Nov 2024 13:32:29 -0500 Subject: [PATCH 05/11] [ignore] Add a few extra assessments in test file for ndo_virtual_port_channel_interfaces. --- .../targets/ndo_virtual_port_channel_interface/tasks/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml b/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml index 6fa14281..73314fc6 100644 --- a/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml +++ b/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml @@ -361,6 +361,8 @@ - query_one.current.name == "ansible_virtual_port_channel_interface_2" - query_all is not changed - query_all.current | length == 2 + - query_all.current.0.name == "ansible_virtual_port_channel_interface_changed" + - query_all.current.1.name == "ansible_virtual_port_channel_interface_2" # DELETE From 6ee15a96ebaacd14ca2e114ad6eaf41958b7ccf0 Mon Sep 17 00:00:00 2001 From: Gaspard Micol Date: Thu, 21 Nov 2024 12:32:20 -0500 Subject: [PATCH 06/11] [ignore] Changed logics, Documentation and Example sections in ndo_virtual_port_channel_interface.py module to be consistent with future modules. --- .../ndo_virtual_port_channel_interface.py | 277 +++++++++--------- 1 file changed, 142 insertions(+), 135 deletions(-) diff --git a/plugins/modules/ndo_virtual_port_channel_interface.py b/plugins/modules/ndo_virtual_port_channel_interface.py index eadb381b..30fa5844 100644 --- a/plugins/modules/ndo_virtual_port_channel_interface.py +++ b/plugins/modules/ndo_virtual_port_channel_interface.py @@ -25,101 +25,101 @@ author: - Gaspard Micol (@gmicol) options: - template: - description: - - The name of the template. - - The template must be a fabric resource template. - type: str - required: true - virtual_port_channel_interface: + template: + description: + - The name of the template. + - The template must be a Fabric Resource template. + type: str + required: true + name: + description: + - The name of the Virtual Port Channel Interface. + type: str + aliases: [ virtual_port_channel_interface, virtual_port_channel, vpc ] + uuid: + description: + - The UUID of the Virtual Port Channel Interface. + - This parameter can be used instead of O(virtual_port_channel_interface) + when an existing Virtual Port Channel Interface is updated. + - This parameter is required when parameter O(name) is updated. + type: str + aliases: [ virtual_port_channel_interface_uuid, virtual_port_channel_uuid, vpc_uuid ] + description: + description: + - The description of the Virtual Port Channel Interface. + type: str + node_1: + description: + - The first node ID. + type: str + node_2: + description: + - The second node ID. + type: str + interfaces_node_1: + description: + - The list of used Interface IDs for the first node. + - Ranges of Interface IDs can be used. + - This parameter is required when creating a new Virtual Port Channel Interface. + type: list + elements: str + aliases: [ interfaces_1, members_1 ] + interfaces_node_2: + description: + - The list of used Interface IDs for the second node. + - Ranges of Interface IDs can be used. + - This parameter is required when creating a new Virtual Port Channel Interface. + type: list + elements: str + aliases: [ interfaces_2, members_2 ] + interface_policy_group_uuid: + description: + - The UUID of the Port Channel Interface Policy Group. + - This parameter is required when creating a new Virtual Port Channel Interface. + type: str + aliases: [ policy_uuid, interface_policy_uuid, interface_setting_uuid ] + interface_policy_group: + description: + - The Port Channel Interface Policy Group. + - This parameter can be used instead of O(interface_policy_group_uuid). + - If both parameter are used, O(interface_policy_group) will be ignored. + type: dict + suboptions: + name: description: - - The name of the Virtual Port Channel Interface. + - The name of the Interface Policy Group. type: str - aliases: [ name, virtual_port_channel, vpc ] - virtual_port_channel_interface_uuid: + template: description: - - The UUID of the Virtual Port Channel Interface. - - This parameter can be used instead of O(virtual_port_channel_interface) - when an existing Virtual Port Channel Interface is updated. - - This parameter is required when parameter O(virtual_port_channel_interface) is updated. + - The name of the template in which the Interface Policy Group has been created. type: str - aliases: [ uuid, virtual_port_channel_uuid, vpc_uuid ] + aliases: [ policy, interface_policy, interface_setting ] + interface_descriptions: description: + - The list of interface descriptions. + type: list + elements: dict + suboptions: + node: description: - - The description of the Virtual Port Channel Interface. + - The node ID. type: str - node_1: + interface_id: description: - - The first node ID. + - The interface ID. type: str - node_2: + description: description: - - The second node ID. + - The description of the interface. type: str - interfaces_node_1: - description: - - The list of used Interface IDs for the first node. - - Ranges of Interface IDs can be used. - - This parameter is required when creating a new Virtual Port Channel Interface. - type: list - elements: str - aliases: [ interfaces_1, members_1 ] - interfaces_node_2: - description: - - The list of used Interface IDs for the second node. - - Ranges of Interface IDs can be used. - - This parameter is required when creating a new Virtual Port Channel Interface. - type: list - elements: str - aliases: [ interfaces_2, members_2 ] - interface_policy_group_uuid: - description: - - The UUID of the Port Channel Interface Policy Group. - - This parameter is required when creating a new Virtual Port Channel Interface. - type: str - aliases: [ policy_uuid, interface_policy_uuid, interface_setting_uuid ] - interface_policy_group: - description: - - The name of the Port Channel Interface Policy Group. - - This parameter can be used instead of O(interface_policy_group_uuid). - - If both parameter are used, O(interface_policy_group) will be ignored. - type: dict - suboptions: - name: - description: - - The name of the Port Channel Interface Policy Group. - type: str - template: - description: - - The name of the template in which the Port Channel Interface Policy Group has been created. - type: str - aliases: [ policy, interface_policy, interface_setting ] - interface_descriptions: - description: - - The list of interface descriptions. - type: list - elements: dict - suboptions: - node: - description: - - The node ID. - type: str - interface_id: - description: - - The interface ID. - type: str - description: - description: - - The description of the interface. - type: str - state: - description: - - Use C(absent) for removing. - - Use C(query) for listing an object or multiple objects. - - Use C(present) for creating or updating. - type: str - choices: [ absent, query, present ] - default: query + state: + description: + - Use C(absent) for removing. + - Use C(query) for listing an object or multiple objects. + - Use C(present) for creating or updating. + type: str + choices: [ absent, query, present ] + default: query extends_documentation_fragment: cisco.mso.modules """ @@ -131,7 +131,7 @@ password: SomeSecretPassword template: ansible_fabric_resource_template description: My Ansible Port Channel - virtual_port_channel_interface: ansible_virtual_port_channel_interface + name: ansible_virtual_port_channel_interface node_1: 101 node_2: 102 interfaces_node_1: @@ -156,38 +156,39 @@ interface_id: 1/2 description: My first Ansible Interface for second node state: present + register: virtual_port_channel_interface_1 -- name: Update a Virtual Port Channel Interface's name +- name: Update a Virtual Port Channel Interface's name with UUID cisco.mso.ndo_virtual_port_channel_interface: host: mso_host username: admin password: SomeSecretPassword template: ansible_fabric_resource_template - virtual_port_channel_interface: ansible_virtual_port_channel_interface_changed - virtual_port_channel_interface_uuid: 0134c73f-4427-4109-9eea-5110ecdf10ea + name: ansible_virtual_port_channel_interface_changed + uuid: "{{ virtual_port_channel_interface_1.current.uuid }}" state: present -- name: Query a Virtual Port Channel Interface using its name in the template +- name: Query a Virtual Port Channel Interface with name cisco.mso.ndo_virtual_port_channel_interface: host: mso_host username: admin password: SomeSecretPassword template: ansible_fabric_resource_template - virtual_port_channel_interface: ansible_virtual_port_channel_interface_changed + name: ansible_virtual_port_channel_interface_changed state: query register: query_one -- name: Query a Virtual Port Channel Interface using its UUID in the template +- name: Query a Virtual Port Channel Interface with UUID cisco.mso.ndo_virtual_port_channel_interface: host: mso_host username: admin password: SomeSecretPassword template: ansible_fabric_resource_template - virtual_port_channel_interface_uuid: 0134c73f-4427-4109-9eea-5110ecdf10ea + uuid: "{{ virtual_port_channel_interface_1.current.uuid }}" state: query register: query_one_uuid -- name: Query all Virtual Port Channel Interfaces in the template +- name: Query all Virtual Port Channel Interfaces in a Fabric Resource Template cisco.mso.ndo_virtual_port_channel_interface: host: mso_host username: admin @@ -196,22 +197,22 @@ state: query register: query_all -- name: Delete a Virtual Port Channel Interface using its name +- name: Delete a Virtual Port Channel Interface with name cisco.mso.ndo_virtual_port_channel_interface: host: mso_host username: admin password: SomeSecretPassword template: ansible_fabric_resource_template - virtual_port_channel_interface: ansible_virtual_port_channel_interface_changed + name: ansible_virtual_port_channel_interface_changed state: absent -- name: Delete a Virtual Port Channel Interface using its UUID +- name: Delete a Virtual Port Channel Interface with UUID cisco.mso.ndo_virtual_port_channel_interface: host: mso_host username: admin password: SomeSecretPassword template: ansible_fabric_resource_template - virtual_port_channel_interface_uuid: 0134c73f-4427-4109-9eea-5110ecdf10ea + uuid: "{{ virtual_port_channel_interface_1.current.uuid }}" state: absent """ @@ -235,8 +236,8 @@ def main(): argument_spec = mso_argument_spec() argument_spec.update( template=dict(type="str", required=True), - virtual_port_channel_interface=dict(type="str", aliases=["name", "virtual_port_channel", "vpc"]), - virtual_port_channel_interface_uuid=dict(type="str", aliases=["uuid", "virtual_port_channel_uuid", "vpc_uuid"]), + name=dict(type="str", aliases=["virtual_port_channel_interface", "virtual_port_channel", "vpc"]), + uuid=dict(type="str", aliases=["virtual_port_channel_interface_uuid", "virtual_port_channel_uuid", "vpc_uuid"]), description=dict(type="str"), node_1=dict(type="str"), node_2=dict(type="str"), @@ -267,16 +268,16 @@ def main(): argument_spec=argument_spec, supports_check_mode=True, required_if=[ - ["state", "absent", ["virtual_port_channel_interface", "virtual_port_channel_interface_uuid"], True], - ["state", "present", ["virtual_port_channel_interface", "virtual_port_channel_interface_uuid"], True], + ["state", "absent", ["name", "uuid"], True], + ["state", "present", ["name", "uuid"], True], ], ) mso = MSOModule(module) template = module.params.get("template") - virtual_port_channel_interface = module.params.get("virtual_port_channel_interface") - virtual_port_channel_interface_uuid = module.params.get("virtual_port_channel_interface_uuid") + name = module.params.get("name") + uuid = module.params.get("uuid") description = module.params.get("description") node_1 = module.params.get("node_1") node_2 = module.params.get("node_2") @@ -300,11 +301,16 @@ def main(): path = "/fabricResourceTemplate/template/virtualPortChannels" existing_virtual_port_channel_interfaces = mso_template.template.get("fabricResourceTemplate", {}).get("template", {}).get("virtualPortChannels", []) - if virtual_port_channel_interface or virtual_port_channel_interface_uuid: + + if state in ["query", "absent"] and existing_virtual_port_channel_interfaces == []: + mso.exit_json() + elif state == "query" and not (name or uuid): + mso.existing = existing_virtual_port_channel_interfaces + elif existing_virtual_port_channel_interfaces and (name or uuid): match = mso_template.get_object_by_key_value_pairs( object_description, existing_virtual_port_channel_interfaces, - [KVPair("uuid", virtual_port_channel_interface_uuid) if virtual_port_channel_interface_uuid else KVPair("name", virtual_port_channel_interface)], + [KVPair("uuid", uuid) if uuid else KVPair("name", name)], ) if match: mso.existing = mso.previous = copy.deepcopy(match.details) @@ -312,54 +318,58 @@ def main(): mso.existing = mso.previous = existing_virtual_port_channel_interfaces if state == "present": + if uuid and not mso.existing: + mso.fail_json(msg="{0} with the UUID: '{1}' not found".format(object_description, uuid)) if interface_policy_group and not interface_policy_group_uuid: fabric_policy_template = MSOTemplate(mso, "fabric_policy", interface_policy_group.get("template")) fabric_policy_template.validate_template("fabricPolicy") interface_policy_group_uuid = fabric_policy_template.get_interface_policy_group_uuid(interface_policy_group.get("name")) - if match: - if virtual_port_channel_interface and match.details.get("name") != virtual_port_channel_interface: - ops.append(dict(op="replace", path="{0}/{1}/name".format(path, match.index), value=virtual_port_channel_interface)) - match.details["name"] = virtual_port_channel_interface + if mso.existing: + proposed_payload = copy.deepcopy(match.details) - if description is not None and match.details.get("description") != description: + if name and mso.existing.get("name") != name: + ops.append(dict(op="replace", path="{0}/{1}/name".format(path, match.index), value=name)) + proposed_payload["name"] = name + + if description is not None and mso.existing.get("description") != description: ops.append(dict(op="replace", path="{0}/{1}/description".format(path, match.index), value=description)) - match.details["description"] = description + proposed_payload["description"] = description - if node_1 is not None and match.details.get("node1Details", {}).get("node") != node_1: + if node_1 is not None and mso.existing.get("node1Details", {}).get("node") != node_1: ops.append(dict(op="replace", path="{0}/{1}/node1Details/node".format(path, match.index), value=node_1)) - match.details["node1Details"]["node"] = node_1 + proposed_payload["node1Details"]["node"] = node_1 - if node_2 is not None and match.details.get("node2Details", {}).get("node") != node_2: + if node_2 is not None and mso.existing.get("node2Details", {}).get("node") != node_2: ops.append(dict(op="replace", path="{0}/{1}/node2Details/node".format(path, match.index), value=node_2)) - match.details["node2Details"]["node"] = node_2 + proposed_payload["node2Details"]["node"] = node_2 - if interface_policy_group_uuid and match.details.get("policy") != interface_policy_group_uuid: + if interface_policy_group_uuid and mso.existing.get("policy") != interface_policy_group_uuid: ops.append(dict(op="replace", path="{0}/{1}/policy".format(path, match.index), value=interface_policy_group_uuid)) - match.details["policy"] = interface_policy_group_uuid + proposed_payload["policy"] = interface_policy_group_uuid - if interfaces_node_1 and interfaces_node_1 != match.details.get("node1Details", {}).get("memberInterfaces"): + if interfaces_node_1 and interfaces_node_1 != mso.existing.get("node1Details", {}).get("memberInterfaces"): ops.append(dict(op="replace", path="{0}/{1}/node1Details/memberInterfaces".format(path, match.index), value=interfaces_node_1)) - match.details["node1Details"]["memberInterfaces"] = interfaces_node_1 + proposed_payload["node1Details"]["memberInterfaces"] = interfaces_node_1 - if interfaces_node_2 and interfaces_node_2 != match.details.get("node2Details", {}).get("memberInterfaces"): + if interfaces_node_2 and interfaces_node_2 != mso.existing.get("node2Details", {}).get("memberInterfaces"): ops.append(dict(op="replace", path="{0}/{1}/node2Details/memberInterfaces".format(path, match.index), value=interfaces_node_2)) - match.details["node2Details"]["memberInterfaces"] = interfaces_node_2 + proposed_payload["node2Details"]["memberInterfaces"] = interfaces_node_2 if interface_descriptions: interface_descriptions = format_interface_descriptions(interface_descriptions) - if interface_descriptions != match.details.get("interfaceDescriptions"): + if interface_descriptions != mso.existing.get("interfaceDescriptions"): ops.append(dict(op="replace", path="{0}/{1}/interfaceDescriptions".format(path, match.index), value=interface_descriptions)) - match.details["interfaceDescriptions"] = interface_descriptions - elif interface_descriptions == [] and match.details["interfaceDescriptions"]: + proposed_payload["interfaceDescriptions"] = interface_descriptions + elif interface_descriptions == [] and mso.existing.get("interfaceDescriptions"): ops.append(dict(op="remove", path="{0}/{1}/interfaceDescriptions".format(path, match.index))) - mso.sanitize(match.details) + mso.sanitize(proposed_payload, collate=True) else: payload = { - "name": virtual_port_channel_interface, + "name": name, "node1Details": { "node": node_1, "memberInterfaces": interfaces_node_1, @@ -369,18 +379,15 @@ def main(): "memberInterfaces": interfaces_node_2, }, "policy": interface_policy_group_uuid, + "description": description, + "interfaceDescriptions": format_interface_descriptions(interface_descriptions), } - if description is not None: - payload["description"] = description - if interface_descriptions: - payload["interfaceDescriptions"] = format_interface_descriptions(interface_descriptions) - - ops.append(dict(op="add", path="{0}/-".format(path), value=payload)) - mso.sanitize(payload) + ops.append(dict(op="add", path="{0}/-".format(path), value=mso.sent)) + elif state == "absent": - if match: + if mso.existing: ops.append(dict(op="remove", path="{0}/{1}".format(path, match.index))) if not module.check_mode and ops: @@ -389,7 +396,7 @@ def main(): match = mso_template.get_object_by_key_value_pairs( object_description, virtual_port_channel_interfaces, - [KVPair("uuid", virtual_port_channel_interface_uuid) if virtual_port_channel_interface_uuid else KVPair("name", virtual_port_channel_interface)], + [KVPair("uuid", uuid) if uuid else KVPair("name", name)], ) if match: mso.existing = match.details From 0090c211a5ae2ea64e031037bbac8cc61cdb593b Mon Sep 17 00:00:00 2001 From: Gaspard Micol Date: Tue, 10 Dec 2024 09:47:19 -0500 Subject: [PATCH 07/11] [ignore] Modify Documentation, required attributes in ndo_virtual_port_channel_interface module. enhance format_interface_descriptions function to format range of interface IDs. --- .../ndo_virtual_port_channel_interface.py | 46 +++--- .../tasks/main.yml | 153 ++++++++++++++++-- 2 files changed, 167 insertions(+), 32 deletions(-) diff --git a/plugins/modules/ndo_virtual_port_channel_interface.py b/plugins/modules/ndo_virtual_port_channel_interface.py index 30fa5844..6f6a330c 100644 --- a/plugins/modules/ndo_virtual_port_channel_interface.py +++ b/plugins/modules/ndo_virtual_port_channel_interface.py @@ -89,10 +89,12 @@ description: - The name of the Interface Policy Group. type: str + required: true template: description: - The name of the template in which the Interface Policy Group has been created. type: str + required: true aliases: [ policy, interface_policy, interface_setting ] interface_descriptions: description: @@ -104,10 +106,13 @@ description: - The node ID. type: str + required: true interface_id: description: - - The interface ID. + - The interface ID or a range of IDs. + - Using a range of interface IDs will apply the same O(description) for every ID in range. type: str + required: true description: description: - The description of the interface. @@ -121,6 +126,14 @@ choices: [ absent, query, present ] default: query extends_documentation_fragment: cisco.mso.modules +notes: +- The O(template) must exist before using this module in your playbook. + Use M(cisco.mso.ndo_template) to create the Fabric Resource template. +- The O(interface_policy_group) must exist before using this module in your playbook. + Use M(cisco.mso.ndo_interface_setting) to create the Interface Policy Group of type Port Channel. +seealso: +- module: cisco.mso.ndo_template +- module: cisco.mso.ndo_interface_setting """ EXAMPLES = r""" @@ -145,16 +158,13 @@ interface_descriptions: - node: 101 interface_id: 1/1 - description: My first Ansible Interface for first node + description: My single Ansible Interface for first node - node: 101 - interface_id: 1/10 - description: My second Ansible Interface for first node - - node: 101 - interface_id: 1/11 - description: My third Ansible Interface for first node + interface_id: 1/10-11 + description: My group of Ansible Interface for first node - node: 102 interface_id: 1/2 - description: My first Ansible Interface for second node + description: My single Ansible Interface for second node state: present register: virtual_port_channel_interface_1 @@ -176,7 +186,7 @@ template: ansible_fabric_resource_template name: ansible_virtual_port_channel_interface_changed state: query - register: query_one + register: query_name - name: Query a Virtual Port Channel Interface with UUID cisco.mso.ndo_virtual_port_channel_interface: @@ -186,7 +196,7 @@ template: ansible_fabric_resource_template uuid: "{{ virtual_port_channel_interface_1.current.uuid }}" state: query - register: query_one_uuid + register: query_uuid - name: Query all Virtual Port Channel Interfaces in a Fabric Resource Template cisco.mso.ndo_virtual_port_channel_interface: @@ -246,8 +256,8 @@ def main(): interface_policy_group=dict( type="dict", options=dict( - name=dict(type="str"), - template=dict(type="str"), + name=dict(type="str", required=True), + template=dict(type="str", required=True), ), aliases=["policy", "interface_policy", "interface_setting"], ), @@ -256,8 +266,8 @@ def main(): type="list", elements="dict", options=dict( - node=dict(type="str"), - interface_id=dict(type="str"), + node=dict(type="str", required=True), + interface_id=dict(type="str", required=True), description=dict(type="str"), ), ), @@ -282,10 +292,10 @@ def main(): node_1 = module.params.get("node_1") node_2 = module.params.get("node_2") interfaces_node_1 = module.params.get("interfaces_node_1") - if interfaces_node_1: + if isinstance(interfaces_node_1, list): interfaces_node_1 = ",".join(interfaces_node_1) interfaces_node_2 = module.params.get("interfaces_node_2") - if interfaces_node_2: + if isinstance(interfaces_node_2, list): interfaces_node_2 = ",".join(interfaces_node_2) interface_policy_group = module.params.get("interface_policy_group") interface_policy_group_uuid = module.params.get("interface_policy_group_uuid") @@ -358,7 +368,7 @@ def main(): proposed_payload["node2Details"]["memberInterfaces"] = interfaces_node_2 if interface_descriptions: - interface_descriptions = format_interface_descriptions(interface_descriptions) + interface_descriptions = format_interface_descriptions(mso, interface_descriptions) if interface_descriptions != mso.existing.get("interfaceDescriptions"): ops.append(dict(op="replace", path="{0}/{1}/interfaceDescriptions".format(path, match.index), value=interface_descriptions)) proposed_payload["interfaceDescriptions"] = interface_descriptions @@ -380,7 +390,7 @@ def main(): }, "policy": interface_policy_group_uuid, "description": description, - "interfaceDescriptions": format_interface_descriptions(interface_descriptions), + "interfaceDescriptions": format_interface_descriptions(mso, interface_descriptions), } mso.sanitize(payload) diff --git a/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml b/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml index 73314fc6..6b5b2b1f 100644 --- a/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml +++ b/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml @@ -58,6 +58,29 @@ <<: *template_absent state: present + - name: Ensure fabric policy template does not exist + cisco.mso.ndo_template: &template_policy_absent + <<: *mso_info + name: ansible_fabric_policy_template + template_type: fabric_policy + state: absent + + - name: Create fabric resource template + cisco.mso.ndo_template: + <<: *template_policy_absent + state: present + + - name: Create two Interface policy groups of type port channel + cisco.mso.ndo_interface_setting: + <<: *mso_info + template: ansible_fabric_policy_template + name: "{{ item }}" + interface_type: port_channel + state: present + loop: + - ansible_test_interface_policy_group_port_channel + - ansible_test_interface_policy_group_port_channel_2 + # CREATE - name: Create a new virtual port channel interface (check_mode) @@ -71,8 +94,8 @@ interfaces_1: 1/1 interfaces_2: 1/1 interface_policy_group: - name: Gaspard_Interface_setting_test - template: Gaspard_FP_3.2_test + name: ansible_test_interface_policy_group_port_channel + template: ansible_fabric_policy_template interface_descriptions: - node: 101 interface_id: 1/1 @@ -200,8 +223,8 @@ cisco.mso.ndo_virtual_port_channel_interface: &update_virtual_port_channel_interface_policy_group <<: *update_virtual_port_channel_interface_description interface_policy_group: - name: Gaspard_Interface_setting_test_2 - template: Gaspard_FP_3.2_test + name: ansible_test_interface_policy_group_port_channel_2 + template: ansible_fabric_policy_template register: nm_update_virtual_port_channel_interface_policy_group - name: Update a virtual port channel interface members for first node @@ -249,11 +272,8 @@ interfaces_1: 1/1-2 interface_descriptions: - node: 103 - interface_id: 1/1 - description: new first Ansible interface test for first node - - node: 103 - interface_id: 1/2 - description: new second Ansible interface test for first node + interface_id: 1/1-2 + description: New group of Ansible interfaces test for first node register: nm_delete_virtual_port_channel_interface_member - name: Assert virtual port channel interface update tasks @@ -320,10 +340,10 @@ - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions | length == 2 - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions.0.nodeID == "103" - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions.0.interfaceID == "1/1" - - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions.0.description == "new first Ansible interface test for first node" + - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions.0.description == "New group of Ansible interfaces test for first node" - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions.1.nodeID == "103" - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions.1.interfaceID == "1/2" - - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions.1.description == "new second Ansible interface test for first node" + - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions.1.description == "New group of Ansible interfaces test for first node" # QUERY @@ -337,8 +357,8 @@ interfaces_1: 1/1 interfaces_2: 1/1 interface_policy_group: - name: Gaspard_Interface_setting_test - template: Gaspard_FP_3.2_test + name: ansible_test_interface_policy_group_port_channel_2 + template: ansible_fabric_policy_template state: present - name: Query a virtual port channel interface with template_name @@ -364,6 +384,80 @@ - query_all.current.0.name == "ansible_virtual_port_channel_interface_changed" - query_all.current.1.name == "ansible_virtual_port_channel_interface_2" + # ERRORS + + - name: Create a new virtual port channel interface without valid range IDs in interface descriptions + cisco.mso.ndo_virtual_port_channel_interface: + <<: *mso_info + template: ansible_fabric_resource_template + virtual_port_channel_interface: ansible_port_channel_interface_error + node_1: 101 + node_2: 102 + interfaces_1: 1/1-2 + interfaces_2: 1/1 + interface_policy_group: + name: ansible_test_interface_policy_group_port_channel + template: ansible_fabric_policy_template + interface_descriptions: + - node: 101 + interface_id: 1/2-1 + description: Incorrect Range starting and ending ID values + state: present + ignore_errors: true + register: nm_create_invalid_range + + - name: Create a new virtual port channel interface without valid IDs values in interface descriptions + cisco.mso.ndo_virtual_port_channel_interface: + <<: *mso_info + template: ansible_fabric_resource_template + virtual_port_channel_interface: ansible_port_channel_interface_error + node_1: 101 + node_2: 102 + interfaces_1: 1/1-2 + interfaces_2: 1/1 + interface_policy_group: + name: ansible_test_interface_policy_group_port_channel + template: ansible_fabric_policy_template + interface_descriptions: + - node: 101 + interface_id: invalid_id + description: Invalid ID value + state: present + ignore_errors: true + register: nm_create_invalid_id + + - name: delete first interface policy group of type port channel + cisco.mso.ndo_interface_setting: + <<: *mso_info + template: ansible_fabric_policy_template + name: ansible_test_interface_policy_group_port_channel + interface_type: port_channel + state: absent + + - name: Create a new virtual port channel interface without an existing interface policy group + cisco.mso.ndo_virtual_port_channel_interface: + <<: *mso_info + template: ansible_fabric_resource_template + virtual_port_channel_interface: ansible_port_channel_interface_error + node_1: 101 + node_2: 102 + interfaces_1: 1/1-2 + interfaces_2: 1/1 + interface_policy_group: + name: ansible_test_interface_policy_group_port_channel + template: ansible_fabric_policy_template + state: present + ignore_errors: true + register: nm_create_without_existing_policy + + - name: Assert virtual port channel interface errors tasks + assert: + that: + - nm_create_missing_node.msg == "Missing parameter 'node' for creating a Port Channel Interface" + - nm_create_invalid_range.msg == "Range start is greater than or equal to range stop for range of IDs '1/2-1'" + - nm_create_invalid_range.msg == "Incorrect interface ID or range of IDs. Got 'invalid_id'" + - nm_create_without_existing_policy.msg == "Provided Interface Policy Groups with '[KVPair(key='name', value='ansible_test_interface_policy_group_port_channel')]' not matching existing object(s): ansible_test_interface_policy_group_port_channel_2" + # DELETE - name: Delete a virtual port channel interface (check_mode) @@ -395,9 +489,40 @@ - nm_delete_virtual_port_channel_interface_again is not changed - nm_delete_virtual_port_channel_interface_again.previous == {} - nm_delete_virtual_port_channel_interface_again.current == {} + + + # ERRORS AND NO PORT CHANNEL INTERFACES FOUND + + - name: Query all virtual port channel interfaces in the template when all are deleted + cisco.mso.ndo_virtual_port_channel_interface: + <<: *mso_info + template: ansible_fabric_resource_template + state: query + register: query_all_none + + - name: Update with non-existing UUID + cisco.mso.ndo_virtual_port_channel_interface: + <<: *mso_info + template: ansible_fabric_resource_template + uuid: non-existing-uuid + state: present + ignore_errors: true + register: update_non_existing_uuid + + - name: Assert no Virtual Port Channel Interface found + assert: + that: + - query_all_none is not changed + - query_all_none.current == {} + - update_non_existing_uuid is failed + - update_non_existing_uuid.msg == "Virtual Port Channel Interface with the UUID{{":"}} 'non-existing-uuid' not found" # CLEANUP TEMPLATE - - name: Ensure templates do not exist + - name: Ensure fabric resource template does not exist cisco.mso.ndo_template: <<: *template_absent + + - name: Ensure fabric policy template does not exist + cisco.mso.ndo_template: + <<: *template_policy_absent From b98999d16e572a7b4c112afde3de6ad3fada1971 Mon Sep 17 00:00:00 2001 From: Gaspard Micol Date: Tue, 10 Dec 2024 10:11:55 -0500 Subject: [PATCH 08/11] [ignore] Implement append_update_ops_data function into ndo_virtual_port_channel_interface module. Modify Documentation and Example sections. --- .../ndo_virtual_port_channel_interface.py | 128 ++++++++---------- 1 file changed, 60 insertions(+), 68 deletions(-) diff --git a/plugins/modules/ndo_virtual_port_channel_interface.py b/plugins/modules/ndo_virtual_port_channel_interface.py index 6f6a330c..140b13a9 100644 --- a/plugins/modules/ndo_virtual_port_channel_interface.py +++ b/plugins/modules/ndo_virtual_port_channel_interface.py @@ -98,7 +98,11 @@ aliases: [ policy, interface_policy, interface_setting ] interface_descriptions: description: - - The list of interface descriptions. + - The list of interface descriptions of the Virtual Port Channel Interface. + - Providing a new list of O(interface_descriptions) will completely + replace an existing one from the Virtual Port Channel Interface. + - Providing an empty list will remove the O(interface_descriptions=[]) + from the Virtual Port Channel Interface. type: list elements: dict suboptions: @@ -109,13 +113,14 @@ required: true interface_id: description: - - The interface ID or a range of IDs. - - Using a range of interface IDs will apply the same O(description) for every ID in range. + - The interface ID or a range of interface IDs. + - Using a range of interface IDs will + apply the same O(interface_descriptions.description) for every ID in range. type: str required: true description: description: - - The description of the interface. + - The description of the interface or group of interfaces. type: str state: description: @@ -168,6 +173,32 @@ state: present register: virtual_port_channel_interface_1 +- name: Update a Virtual Port Channel Interface's interfaces and their descriptions + cisco.mso.ndo_port_channel_interface: + host: mso_host + username: admin + password: SomeSecretPassword + description: My Ansible Port Channel + name: ansible_virtual_port_channel_interface + node_1: 101 + node_2: 102 + interfaces_node_1: + - 1/1 + - 1/5-7 + interfaces_node_2: + - 1/1-2 + interface_descriptions: + - node: 101 + interface_id: 1/1 + description: My single unchanged Ansible Interface for first node + - node: 101 + interface_id: 1/5-7 + description: My new group of Ansible Interface for first node + - node: 102 + interface_id: 1/1-2 + description: My new group of Ansible Interfaces for second node + state: present + - name: Update a Virtual Port Channel Interface's name with UUID cisco.mso.ndo_virtual_port_channel_interface: host: mso_host @@ -240,6 +271,7 @@ MSOTemplate, KVPair, ) +from ansible_collections.cisco.mso.plugins.module_utils.utils import append_update_ops_data def main(): @@ -302,15 +334,11 @@ def main(): interface_descriptions = module.params.get("interface_descriptions") state = module.params.get("state") - ops = [] - match = None - mso_template = MSOTemplate(mso, "fabric_resource", template) mso_template.validate_template("fabricResource") - object_description = "Virtual Port Channel Interface" - path = "/fabricResourceTemplate/template/virtualPortChannels" existing_virtual_port_channel_interfaces = mso_template.template.get("fabricResourceTemplate", {}).get("template", {}).get("virtualPortChannels", []) + object_description = "Virtual Port Channel Interface" if state in ["query", "absent"] and existing_virtual_port_channel_interfaces == []: mso.exit_json() @@ -323,9 +351,10 @@ def main(): [KVPair("uuid", uuid) if uuid else KVPair("name", name)], ) if match: + virtual_port_channel_attrs_path = "/fabricResourceTemplate/template/virtualPortChannels/{0}".format(match.index) mso.existing = mso.previous = copy.deepcopy(match.details) - else: - mso.existing = mso.previous = existing_virtual_port_channel_interfaces + + ops = [] if state == "present": if uuid and not mso.existing: @@ -336,69 +365,32 @@ def main(): fabric_policy_template.validate_template("fabricPolicy") interface_policy_group_uuid = fabric_policy_template.get_interface_policy_group_uuid(interface_policy_group.get("name")) - if mso.existing: - proposed_payload = copy.deepcopy(match.details) - - if name and mso.existing.get("name") != name: - ops.append(dict(op="replace", path="{0}/{1}/name".format(path, match.index), value=name)) - proposed_payload["name"] = name - - if description is not None and mso.existing.get("description") != description: - ops.append(dict(op="replace", path="{0}/{1}/description".format(path, match.index), value=description)) - proposed_payload["description"] = description - - if node_1 is not None and mso.existing.get("node1Details", {}).get("node") != node_1: - ops.append(dict(op="replace", path="{0}/{1}/node1Details/node".format(path, match.index), value=node_1)) - proposed_payload["node1Details"]["node"] = node_1 - - if node_2 is not None and mso.existing.get("node2Details", {}).get("node") != node_2: - ops.append(dict(op="replace", path="{0}/{1}/node2Details/node".format(path, match.index), value=node_2)) - proposed_payload["node2Details"]["node"] = node_2 - - if interface_policy_group_uuid and mso.existing.get("policy") != interface_policy_group_uuid: - ops.append(dict(op="replace", path="{0}/{1}/policy".format(path, match.index), value=interface_policy_group_uuid)) - proposed_payload["policy"] = interface_policy_group_uuid - - if interfaces_node_1 and interfaces_node_1 != mso.existing.get("node1Details", {}).get("memberInterfaces"): - ops.append(dict(op="replace", path="{0}/{1}/node1Details/memberInterfaces".format(path, match.index), value=interfaces_node_1)) - proposed_payload["node1Details"]["memberInterfaces"] = interfaces_node_1 - - if interfaces_node_2 and interfaces_node_2 != mso.existing.get("node2Details", {}).get("memberInterfaces"): - ops.append(dict(op="replace", path="{0}/{1}/node2Details/memberInterfaces".format(path, match.index), value=interfaces_node_2)) - proposed_payload["node2Details"]["memberInterfaces"] = interfaces_node_2 - - if interface_descriptions: - interface_descriptions = format_interface_descriptions(mso, interface_descriptions) - if interface_descriptions != mso.existing.get("interfaceDescriptions"): - ops.append(dict(op="replace", path="{0}/{1}/interfaceDescriptions".format(path, match.index), value=interface_descriptions)) - proposed_payload["interfaceDescriptions"] = interface_descriptions - elif interface_descriptions == [] and mso.existing.get("interfaceDescriptions"): - ops.append(dict(op="remove", path="{0}/{1}/interfaceDescriptions".format(path, match.index))) + mso_values = dict( + name=name, + description=description, + node1Details=dict( + node=node_1, + memberInterfaces=interfaces_node_1, + ), + node2Details=dict( + node=node_2, + memberInterfaces=interfaces_node_2, + ), + policy=interface_policy_group_uuid, + interfaceDescriptions=format_interface_descriptions(mso, interface_descriptions), + ) - mso.sanitize(proposed_payload, collate=True) + if mso.existing: + append_update_ops_data(ops, match.details, virtual_port_channel_attrs_path, mso_values) + mso.sanitize(match.details, collate=True) else: - payload = { - "name": name, - "node1Details": { - "node": node_1, - "memberInterfaces": interfaces_node_1, - }, - "node2Details": { - "node": node_2, - "memberInterfaces": interfaces_node_2, - }, - "policy": interface_policy_group_uuid, - "description": description, - "interfaceDescriptions": format_interface_descriptions(mso, interface_descriptions), - } - mso.sanitize(payload) - - ops.append(dict(op="add", path="{0}/-".format(path), value=mso.sent)) + mso.sanitize(mso_values) + ops.append(dict(op="add", path="/fabricResourceTemplate/template/virtualPortChannels/-", value=mso.sent)) elif state == "absent": if mso.existing: - ops.append(dict(op="remove", path="{0}/{1}".format(path, match.index))) + ops.append(dict(op="remove", path=virtual_port_channel_attrs_path)) if not module.check_mode and ops: response = mso.request(mso_template.template_path, method="PATCH", data=ops) From 60c6f694bc893a88dac433b08db7e325e5517e09 Mon Sep 17 00:00:00 2001 From: Gaspard Micol Date: Wed, 11 Dec 2024 04:46:19 -0500 Subject: [PATCH 09/11] [ignore] Modify test case and logic for ndo_virtual_port_channel_interface module when handling null value. --- .../ndo_virtual_port_channel_interface.py | 4 ++-- .../tasks/main.yml | 24 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/plugins/modules/ndo_virtual_port_channel_interface.py b/plugins/modules/ndo_virtual_port_channel_interface.py index 140b13a9..b19a4c35 100644 --- a/plugins/modules/ndo_virtual_port_channel_interface.py +++ b/plugins/modules/ndo_virtual_port_channel_interface.py @@ -340,7 +340,7 @@ def main(): existing_virtual_port_channel_interfaces = mso_template.template.get("fabricResourceTemplate", {}).get("template", {}).get("virtualPortChannels", []) object_description = "Virtual Port Channel Interface" - if state in ["query", "absent"] and existing_virtual_port_channel_interfaces == []: + if state in ["query", "absent"] and not existing_virtual_port_channel_interfaces: mso.exit_json() elif state == "query" and not (name or uuid): mso.existing = existing_virtual_port_channel_interfaces @@ -394,7 +394,7 @@ def main(): if not module.check_mode and ops: response = mso.request(mso_template.template_path, method="PATCH", data=ops) - virtual_port_channel_interfaces = response.get("fabricResourceTemplate", {}).get("template", {}).get("virtualPortChannels", []) + virtual_port_channel_interfaces = response.get("fabricResourceTemplate", {}).get("template", {}).get("virtualPortChannels", []) or [] match = mso_template.get_object_by_key_value_pairs( object_description, virtual_port_channel_interfaces, diff --git a/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml b/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml index 6b5b2b1f..e422f4ca 100644 --- a/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml +++ b/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml @@ -316,7 +316,7 @@ - nm_update_virtual_port_channel_first_interface_members is changed - nm_update_virtual_port_channel_first_interface_members.current.node1Details.memberInterfaces == "1/1-3" - nm_update_virtual_port_channel_second_interface_members is changed - - nm_update_virtual_port_channel_second_interface_members.current.node1Details.memberInterfaces == "1/1-2" + - nm_update_virtual_port_channel_second_interface_members.current.node2Details.memberInterfaces == "1/1-2" - nm_update_virtual_port_channel_interface_descriptions is changed - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions | length == 5 - nm_update_virtual_port_channel_interface_descriptions.current.interfaceDescriptions.0.nodeID == "103" @@ -367,6 +367,14 @@ state: query register: query_one + - name: Query a virtual port channel with template_name and UUID + cisco.mso.ndo_tenant_custom_qos_policy: &query_virtual_port_channel_uuid + <<: *create_virtual_port_channel_interface_2 + uuid: '{{ query_one.current.uuid }}' + name: '{{ fakevar | default(omit)}}' + state: query + register: query_one_uuid + - name: Query all virtual port channel interfaces in the template cisco.mso.ndo_virtual_port_channel_interface: <<: *mso_info @@ -379,6 +387,8 @@ that: - query_one is not changed - query_one.current.name == "ansible_virtual_port_channel_interface_2" + - query_one_uuid is not changed + - query_one_uuid.current.name == "ansible_virtual_port_channel_interface_2" - query_all is not changed - query_all.current | length == 2 - query_all.current.0.name == "ansible_virtual_port_channel_interface_changed" @@ -453,9 +463,8 @@ - name: Assert virtual port channel interface errors tasks assert: that: - - nm_create_missing_node.msg == "Missing parameter 'node' for creating a Port Channel Interface" - nm_create_invalid_range.msg == "Range start is greater than or equal to range stop for range of IDs '1/2-1'" - - nm_create_invalid_range.msg == "Incorrect interface ID or range of IDs. Got 'invalid_id'" + - nm_create_invalid_id.msg == "Incorrect interface ID or range of IDs. Got 'invalid_id'" - nm_create_without_existing_policy.msg == "Provided Interface Policy Groups with '[KVPair(key='name', value='ansible_test_interface_policy_group_port_channel')]' not matching existing object(s): ansible_test_interface_policy_group_port_channel_2" # DELETE @@ -477,6 +486,12 @@ <<: *delete_virtual_port_channel_interface register: nm_delete_virtual_port_channel_interface_again + - name: Delete a virtual port channel with UUID + cisco.mso.ndo_virtual_port_channel_interface: + <<: *query_virtual_port_channel_uuid + state: absent + register: nm_delete_virtual_port_channel_interface_uuid + - name: Assert virtual port channel interface deletion tasks assert: that: @@ -489,6 +504,9 @@ - nm_delete_virtual_port_channel_interface_again is not changed - nm_delete_virtual_port_channel_interface_again.previous == {} - nm_delete_virtual_port_channel_interface_again.current == {} + - nm_delete_virtual_port_channel_interface_uuid is changed + - nm_delete_virtual_port_channel_interface_uuid.previous.name == "ansible_virtual_port_channel_interface_2" + - nm_delete_virtual_port_channel_interface_uuid.current == {} # ERRORS AND NO PORT CHANNEL INTERFACES FOUND From bf9e238129772231fd2913fc8d8452ecb26a31cd Mon Sep 17 00:00:00 2001 From: Gaspard Micol Date: Tue, 17 Dec 2024 13:59:24 -0500 Subject: [PATCH 10/11] [ignore] Add the option to mirror node 1 interfaces for node 2. --- .../ndo_virtual_port_channel_interface.py | 22 ++++++++++++++++++- .../tasks/main.yml | 19 +++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/plugins/modules/ndo_virtual_port_channel_interface.py b/plugins/modules/ndo_virtual_port_channel_interface.py index b19a4c35..8a47902c 100644 --- a/plugins/modules/ndo_virtual_port_channel_interface.py +++ b/plugins/modules/ndo_virtual_port_channel_interface.py @@ -69,6 +69,8 @@ - The list of used Interface IDs for the second node. - Ranges of Interface IDs can be used. - This parameter is required when creating a new Virtual Port Channel Interface. + - If O(interfaces_node_2=[mirror]) is defined as an empty list and O(interfaces_node_1) is clearly defined, + the interfaces of O(interfaces_node_1) will be mirrored in O(interfaces_node_2). type: list elements: str aliases: [ interfaces_2, members_2 ] @@ -199,6 +201,21 @@ description: My new group of Ansible Interfaces for second node state: present +- name: Update a Virtual Port Channel Interface by mirroring node 1 and node 2 interfaces + cisco.mso.ndo_port_channel_interface: + host: mso_host + username: admin + password: SomeSecretPassword + description: My Ansible Port Channel + name: ansible_virtual_port_channel_interface + node_1: 101 + node_2: 102 + interfaces_node_1: + - 1/1-9 + - 1/11-15 + interfaces_node_2: mirror + state: present + - name: Update a Virtual Port Channel Interface's name with UUID cisco.mso.ndo_virtual_port_channel_interface: host: mso_host @@ -328,7 +345,10 @@ def main(): interfaces_node_1 = ",".join(interfaces_node_1) interfaces_node_2 = module.params.get("interfaces_node_2") if isinstance(interfaces_node_2, list): - interfaces_node_2 = ",".join(interfaces_node_2) + if interfaces_node_2[0] == "mirror" and interfaces_node_1: + interfaces_node_2 = interfaces_node_1 + else: + interfaces_node_2 = ",".join(interfaces_node_2) interface_policy_group = module.params.get("interface_policy_group") interface_policy_group_uuid = module.params.get("interface_policy_group_uuid") interface_descriptions = module.params.get("interface_descriptions") diff --git a/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml b/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml index e422f4ca..4a05da1c 100644 --- a/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml +++ b/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml @@ -276,6 +276,13 @@ description: New group of Ansible interfaces test for first node register: nm_delete_virtual_port_channel_interface_member + - name: Update virtual port channel interface members for first node and mirror them on the second node + cisco.mso.ndo_virtual_port_channel_interface: &mirror_virtual_port_channel_interface_members + <<: *delete_virtual_port_channel_interface_member + interfaces_1: 1/1-5 + interfaces_2: mirror + register: nm_mirror_virtual_port_channel_interface_members + - name: Assert virtual port channel interface update tasks assert: that: @@ -344,6 +351,16 @@ - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions.1.nodeID == "103" - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions.1.interfaceID == "1/2" - nm_delete_virtual_port_channel_interface_member.current.interfaceDescriptions.1.description == "New group of Ansible interfaces test for first node" + - nm_mirror_virtual_port_channel_interface_members is changed + - nm_mirror_virtual_port_channel_interface_members.current.node1Details.memberInterfaces == "1/1-5" + - nm_mirror_virtual_port_channel_interface_members.current.node2Details.memberInterfaces == "1/1-5" + - nm_mirror_virtual_port_channel_interface_members.current.interfaceDescriptions | length == 2 + - nm_mirror_virtual_port_channel_interface_members.current.interfaceDescriptions.0.nodeID == "103" + - nm_mirror_virtual_port_channel_interface_members.current.interfaceDescriptions.0.interfaceID == "1/1" + - nm_mirror_virtual_port_channel_interface_members.current.interfaceDescriptions.0.description == "New group of Ansible interfaces test for first node" + - nm_mirror_virtual_port_channel_interface_members.current.interfaceDescriptions.1.nodeID == "103" + - nm_mirror_virtual_port_channel_interface_members.current.interfaceDescriptions.1.interfaceID == "1/2" + - nm_mirror_virtual_port_channel_interface_members.current.interfaceDescriptions.1.description == "New group of Ansible interfaces test for first node" # QUERY @@ -471,7 +488,7 @@ - name: Delete a virtual port channel interface (check_mode) cisco.mso.ndo_virtual_port_channel_interface: &delete_virtual_port_channel_interface - <<: *delete_virtual_port_channel_interface_member + <<: *mirror_virtual_port_channel_interface_members state: absent check_mode: true register: cm_delete_virtual_port_channel_interface From 94aba9f5eb22a563ae3306185c3b279e5d4ea851 Mon Sep 17 00:00:00 2001 From: Gaspard Micol Date: Thu, 19 Dec 2024 10:58:50 -0500 Subject: [PATCH 11/11] [ignore] Modify payload for Node1Details and Node2Details if object is being updated or created for ndo_virtual_port_channel_interface module. --- .../ndo_virtual_port_channel_interface.py | 39 ++++++++++++------- .../tasks/main.yml | 2 +- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/plugins/modules/ndo_virtual_port_channel_interface.py b/plugins/modules/ndo_virtual_port_channel_interface.py index 8a47902c..60cba8ba 100644 --- a/plugins/modules/ndo_virtual_port_channel_interface.py +++ b/plugins/modules/ndo_virtual_port_channel_interface.py @@ -385,26 +385,35 @@ def main(): fabric_policy_template.validate_template("fabricPolicy") interface_policy_group_uuid = fabric_policy_template.get_interface_policy_group_uuid(interface_policy_group.get("name")) - mso_values = dict( - name=name, - description=description, - node1Details=dict( - node=node_1, - memberInterfaces=interfaces_node_1, - ), - node2Details=dict( - node=node_2, - memberInterfaces=interfaces_node_2, - ), - policy=interface_policy_group_uuid, - interfaceDescriptions=format_interface_descriptions(mso, interface_descriptions), - ) - if mso.existing: + mso_values = { + "name": name, + "description": description, + ("node1Details", "node"): node_1, + ("node1Details", "memberInterfaces"): interfaces_node_1, + ("node2Details", "node"): node_2, + ("node2Details", "memberInterfaces"): interfaces_node_2, + "policy": interface_policy_group_uuid, + "interfaceDescriptions": format_interface_descriptions(mso, interface_descriptions), + } append_update_ops_data(ops, match.details, virtual_port_channel_attrs_path, mso_values) mso.sanitize(match.details, collate=True) else: + mso_values = dict( + name=name, + description=description, + node1Details=dict( + node=node_1, + memberInterfaces=interfaces_node_1, + ), + node2Details=dict( + node=node_2, + memberInterfaces=interfaces_node_2, + ), + policy=interface_policy_group_uuid, + interfaceDescriptions=format_interface_descriptions(mso, interface_descriptions), + ) mso.sanitize(mso_values) ops.append(dict(op="add", path="/fabricResourceTemplate/template/virtualPortChannels/-", value=mso.sent)) diff --git a/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml b/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml index 4a05da1c..7688c520 100644 --- a/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml +++ b/tests/integration/targets/ndo_virtual_port_channel_interface/tasks/main.yml @@ -385,7 +385,7 @@ register: query_one - name: Query a virtual port channel with template_name and UUID - cisco.mso.ndo_tenant_custom_qos_policy: &query_virtual_port_channel_uuid + cisco.mso.ndo_virtual_port_channel_interface: &query_virtual_port_channel_uuid <<: *create_virtual_port_channel_interface_2 uuid: '{{ query_one.current.uuid }}' name: '{{ fakevar | default(omit)}}'