diff --git a/plugins/module_utils/mso.py b/plugins/module_utils/mso.py index f66d4cbb..ad95d039 100644 --- a/plugins/module_utils/mso.py +++ b/plugins/module_utils/mso.py @@ -318,6 +318,21 @@ def write_file(module, url, dest, content, resp, tmpsrc=None): os.remove(tmpsrc) +def format_interface_descriptions(interface_descriptions, node=None): + if interface_descriptions: + formated_interface_descriptions = [ + { + "nodeID": node if node is not None else interface_description.get("node"), + "interfaceID": interface_description.get("interface_id", interface_description.get("interfaceID")), + "description": interface_description.get("description"), + } + for interface_description in interface_descriptions + ] + else: + formated_interface_descriptions = [] + return formated_interface_descriptions + + class MSOModule(object): def __init__(self, module): self.module = module diff --git a/plugins/module_utils/template.py b/plugins/module_utils/template.py index 55d235b4..011e5e7d 100644 --- a/plugins/module_utils/template.py +++ b/plugins/module_utils/template.py @@ -207,3 +207,14 @@ def get_l3out_node_routing_policy_object(self, uuid=None, name=None, fail_module "L3Out Node Routing Policy", existing_l3out_node_routing_policy, [KVPair("uuid", uuid) if uuid else KVPair("name", name)], fail_module ) return existing_l3out_node_routing_policy # Query all objects + + def get_interface_policy_group_uuid(self, interface_policy_group): + """ + Get the UUID of an Interface Policy Group by name. + :param interface_policy_group: Name of the Interface Policy Group to search for -> Str + :return: UUID of the Interface Policy Group. -> Str + """ + existing_policy_groups = self.template.get("fabricPolicyTemplate", {}).get("template", {}).get("interfacePolicyGroups", []) + kv_list = [KVPair("name", interface_policy_group)] + match = self.get_object_by_key_value_pairs("Interface Policy Groups", existing_policy_groups, kv_list, fail_module=True) + return match.details.get("uuid") diff --git a/plugins/modules/ndo_port_channel_interface.py b/plugins/modules/ndo_port_channel_interface.py new file mode 100644 index 00000000..3869701a --- /dev/null +++ b/plugins/modules/ndo_port_channel_interface.py @@ -0,0 +1,372 @@ +#!/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_port_channel_interface +short_description: Manage Port Channel Interfaces on Cisco Nexus Dashboard Orchestrator (NDO). +description: +- Manage 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) +options: + 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 Port Channel Interface. + type: str + aliases: [ port_channel_interface, port_channel ] + uuid: + description: + - The UUID of the Port Channel Interface. + - This parameter can be used instead of O(port_channel_interface) + when an existing Virtual Port Channel Interface is updated. + - This parameter is required when parameter O(port_channel_interface) is updated. + type: str + aliases: [ port_channel_interface_uuid, port_channel_uuid ] + description: + description: + - The description of the Port Channel Interface. + type: str + node: + description: + - The node ID. + - This is only required when creating a new Port Channel Interface. + type: str + interfaces: + description: + - The list of used Interface IDs. + - Ranges of Interface IDs can be used. + - This is only required when creating a new Port Channel Interface. + type: list + elements: str + aliases: [ members ] + interface_policy_group_uuid: + description: + - The UUID of the Port Channel Interface Policy Group. + - This is only required when creating a new 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 Interface Policy Group. + type: str + template: + description: + - The name of the template in which the 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: + 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 Port Channel Interface + cisco.mso.ndo_port_channel_interface: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_resource_template + description: My Ansible Port Channel + name: ansible_port_channel_interface + node: 101 + interfaces: + - 1/1 + - 1/10-11 + interface_policy_group: + name: ansible_policy_group + template: ansible_fabric_policy_template + interface_descriptions: + - interface_id: 1/1 + description: My first Ansible Interface + - interface_id: 1/10 + description: My second Ansible Interface + - interface_id: 1/11 + description: My third Ansible Interface + state: present + register: port_channel_interface_1 + +- name: Update a Port Channel Interface's name with UUID + cisco.mso.ndo_port_channel_interface: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_resource_template + name: ansible_port_channel_interface_changed + uuid: "{{ port_channel_interface_1.current.uuid }}" + state: present + +- name: Query a Port Channel Interface with name + cisco.mso.ndo_port_channel_interface: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_resource_template + name: ansible_port_channel_interface_changed + state: query + register: query_one + +- name: Query a Port Channel Interface with UUID + cisco.mso.ndo_port_channel_interface: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_resource_template + uuid: "{{ port_channel_interface_1.current.uuid }}" + state: query + register: query_one + +- name: Query all Port Channel Interfaces in a Fabric Resource Template + cisco.mso.ndo_port_channel_interface: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_resource_template + state: query + register: query_all + +- name: Delete a Port Channel Interface with name + cisco.mso.ndo_port_channel_interface: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_resource_template + name: ansible_port_channel_interface_changed + state: absent + +- name: Delete a Port Channel Interface with UUID + cisco.mso.ndo_port_channel_interface: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_resource_template + uuid: "{{ port_channel_interface_1.current.uuid }}" + 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, + format_interface_descriptions, +) +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), + name=dict(type="str", aliases=["port_channel_interface", "port_channel"]), + uuid=dict(type="str", aliases=["port_channel_interface_uuid", "port_channel_uuid"]), + description=dict(type="str"), + node=dict(type="str"), + interfaces=dict(type="list", elements="str", aliases=["members"]), + interface_policy_group=dict( + type="dict", + options=dict( + name=dict(type="str"), + template=dict(type="str"), + ), + aliases=["policy", "interface_policy", "interface_setting"], + ), + interface_policy_group_uuid=dict(type="str", aliases=["policy_uuid", "interface_policy_uuid", "interface_setting_uuid"]), + interface_descriptions=dict( + type="list", + elements="dict", + options=dict( + 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", ["name", "uuid"], True], + ["state", "present", ["name", "uuid"], True], + ], + ) + + mso = MSOModule(module) + + template = module.params.get("template") + name = module.params.get("name") + uuid = module.params.get("uuid") + description = module.params.get("description") + node = module.params.get("node") + interfaces = module.params.get("interfaces") + if interfaces: + interfaces = ",".join(interfaces) + 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") + + mso_template = MSOTemplate(mso, "fabric_resource", template) + mso_template.validate_template("fabricResource") + object_description = "Port Channel Interface" + + path = "/fabricResourceTemplate/template/portChannels" + existing_port_channel_interfaces = mso_template.template.get("fabricResourceTemplate", {}).get("template", {}).get("portChannels", []) + + if state in ["query", "absent"] and existing_port_channel_interfaces == []: + mso.exit_json() + elif state == "query" and not (name or uuid): + mso.existing = existing_port_channel_interfaces + elif existing_port_channel_interfaces and (name or uuid): + match = mso_template.get_object_by_key_value_pairs( + object_description, + existing_port_channel_interfaces, + [KVPair("uuid", uuid) if uuid else KVPair("name", name)], + ) + if match: + mso.existing = mso.previous = copy.deepcopy(match.details) + + ops = [] + + 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 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 + + node_changed = False + if node and mso.existing.get("node") != node: + ops.append(dict(op="replace", path="{0}/{1}/node".format(path, match.index), value=node)) + proposed_payload["node"] = node + node_changed = True + + 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 and interfaces != mso.existing.get("memberInterfaces"): + ops.append(dict(op="replace", path="{0}/{1}/memberInterfaces".format(path, match.index), value=interfaces)) + proposed_payload["memberInterfaces"] = interfaces + + if interface_descriptions or (node_changed and mso.existing.get("interfaceDescriptions")): + if node_changed and interface_descriptions is None: + interface_descriptions = format_interface_descriptions(mso.existing["interfaceDescriptions"], node) + else: + interface_descriptions = format_interface_descriptions(interface_descriptions, proposed_payload["node"]) + 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))) + proposed_payload["interfaceDescriptions"] = [] + + mso.sanitize(proposed_payload, collate=True) + + else: + if not node: + mso.fail_json(msg=("ERROR: Missing parameter 'node' for creating a Port Channel Interface")) + payload = { + "name": name, + "node": node, + "memberInterfaces": interfaces, + "policy": interface_policy_group_uuid, + "description": description, + "interfaceDescriptions": format_interface_descriptions(interface_descriptions, node), + } + + mso.sanitize(payload) + ops.append(dict(op="add", path="{0}/-".format(path), value=mso.sent)) + + elif state == "absent": + if mso.existing: + 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) + port_channel_interfaces = response.get("fabricResourceTemplate", {}).get("template", {}).get("portChannels", []) + match = mso_template.get_object_by_key_value_pairs( + object_description, + port_channel_interfaces, + [KVPair("uuid", uuid) if uuid else KVPair("name", name)], + ) + 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_port_channel_interface/aliases b/tests/integration/targets/ndo_port_channel_interface/aliases new file mode 100644 index 00000000..5042c9c0 --- /dev/null +++ b/tests/integration/targets/ndo_port_channel_interface/aliases @@ -0,0 +1,2 @@ +# No ACI MultiSite infrastructure, so not enabled +# unsupported diff --git a/tests/integration/targets/ndo_port_channel_interface/tasks/main.yml b/tests/integration/targets/ndo_port_channel_interface/tasks/main.yml new file mode 100644 index 00000000..810499b3 --- /dev/null +++ b/tests/integration/targets/ndo_port_channel_interface/tasks/main.yml @@ -0,0 +1,354 @@ +# 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 port channel interface (check_mode) + cisco.mso.ndo_port_channel_interface: &create_port_channel_interface + <<: *mso_info + template: ansible_fabric_resource_template + port_channel_interface: ansible_port_channel_interface + description: Ansible Port Channel test + node: 101 + interfaces: 1/1 + interface_policy_group: + name: Gaspard_Interface_setting_test + template: Gaspard_FP_3.2_test + interface_descriptions: + - interface_id: 1/1 + description: first Ansible interface test + state: present + check_mode: true + register: cm_create_new_port_channel_interface + + - name: Create a new port channel interface + cisco.mso.ndo_port_channel_interface: + <<: *create_port_channel_interface + register: nm_create_new_port_channel_interface + + - name: Create a new port channel interface again + cisco.mso.ndo_port_channel_interface: + <<: *create_port_channel_interface + register: nm_create_new_port_channel_interface_again + + - name: Assert port channel interface creation tasks + assert: + that: + - cm_create_new_port_channel_interface is changed + - cm_create_new_port_channel_interface.previous == {} + - cm_create_new_port_channel_interface.current.name == "ansible_port_channel_interface" + - cm_create_new_port_channel_interface.current.description == "Ansible Port Channel test" + - cm_create_new_port_channel_interface.current.node == "101" + - cm_create_new_port_channel_interface.current.memberInterfaces == "1/1" + - cm_create_new_port_channel_interface.current.interfaceDescriptions | length == 1 + - cm_create_new_port_channel_interface.current.interfaceDescriptions.0.nodeID == "101" + - cm_create_new_port_channel_interface.current.interfaceDescriptions.0.interfaceID == "1/1" + - cm_create_new_port_channel_interface.current.interfaceDescriptions.0.description == "first Ansible interface test" + - nm_create_new_port_channel_interface is changed + - nm_create_new_port_channel_interface.previous == {} + - nm_create_new_port_channel_interface.current.name == "ansible_port_channel_interface" + - nm_create_new_port_channel_interface.current.description == "Ansible Port Channel test" + - nm_create_new_port_channel_interface.current.node == "101" + - nm_create_new_port_channel_interface.current.memberInterfaces == "1/1" + - nm_create_new_port_channel_interface.current.interfaceDescriptions | length == 1 + - nm_create_new_port_channel_interface.current.interfaceDescriptions.0.nodeID == "101" + - nm_create_new_port_channel_interface.current.interfaceDescriptions.0.interfaceID == "1/1" + - nm_create_new_port_channel_interface.current.interfaceDescriptions.0.description == "first Ansible interface test" + - nm_create_new_port_channel_interface_again is not changed + - nm_create_new_port_channel_interface_again.previous == nm_create_new_port_channel_interface_again.current + - nm_create_new_port_channel_interface_again.current.name == "ansible_port_channel_interface" + - nm_create_new_port_channel_interface_again.current.description == "Ansible Port Channel test" + - nm_create_new_port_channel_interface_again.current.node == "101" + - nm_create_new_port_channel_interface_again.current.memberInterfaces == "1/1" + - nm_create_new_port_channel_interface_again.current.interfaceDescriptions | length == 1 + - nm_create_new_port_channel_interface_again.current.interfaceDescriptions.0.nodeID == "101" + - nm_create_new_port_channel_interface_again.current.interfaceDescriptions.0.interfaceID == "1/1" + - nm_create_new_port_channel_interface_again.current.interfaceDescriptions.0.description == "first Ansible interface test" + + # CREATION ERRORS + + - name: Create a new port channel interface without a node + cisco.mso.ndo_port_channel_interface: + <<: *mso_info + template: ansible_fabric_resource_template + port_channel_interface: ansible_port_channel_interface_error + interfaces: 1/1 + interface_policy: Gaspard_Interface_setting_test + state: present + ignore_errors: true + register: nm_create_missing_node + + - name: Assert port channel interface creation errors tasks + assert: + that: + - nm_create_missing_node.msg == "ERROR: Missing 'node' for creating a Port Channel Interface" + + # UPDATE + + - name: Update a port channel interface node (check_mode) + cisco.mso.ndo_port_channel_interface: &update_port_channel_interface + <<: *create_port_channel_interface + node: 102 + check_mode: true + register: cm_update_port_channel_interface_node + + - name: Update a port channel interface node + cisco.mso.ndo_port_channel_interface: + <<: *update_port_channel_interface + register: nm_update_port_channel_interface_node + + - name: Update a port channel interface node again + cisco.mso.ndo_port_channel_interface: + <<: *update_port_channel_interface + register: nm_update_port_channel_interface_node_again + + - name: Update a port channel interface node without previous configurations + cisco.mso.ndo_port_channel_interface: &update_port_channel_interface_node + <<: *mso_info + template: ansible_fabric_resource_template + port_channel_interface: ansible_port_channel_interface + node: 103 + state: present + register: nm_update_node_without_previous_config + + - name: Update a port channel interface name + cisco.mso.ndo_port_channel_interface: &update_port_channel_interface_name + <<: *update_port_channel_interface_node + port_channel_interface_uuid: '{{ nm_update_port_channel_interface_node.current.uuid }}' + port_channel_interface: ansible_port_channel_interface_changed + register: nm_update_port_channel_interface_name + + - name: Update a port channel interface description + cisco.mso.ndo_port_channel_interface: &update_port_channel_interface_description + <<: *update_port_channel_interface_name + description: Ansible Port Channel test updated + register: nm_update_port_channel_interface_description + + - name: Update a port channel interface policy + cisco.mso.ndo_port_channel_interface: &update_port_channel_interface_policy + <<: *update_port_channel_interface_description + interface_policy_group: + name: Gaspard_Interface_setting_test_2 + template: Gaspard_FP_3.2_test + register: nm_update_port_channel_interface_policy + + - name: Update a port channel interface members + cisco.mso.ndo_port_channel_interface: &update_port_channel_interface_members + <<: *update_port_channel_interface_policy + interfaces: + - 1/1-3 + - 1/8-10 + - 2/3-5 + register: nm_update_port_channel_interface_members + + - name: Update a port channel interface members descriptions + cisco.mso.ndo_port_channel_interface: &update_port_channel_interface_descriptions + <<: *update_port_channel_interface_members + interface_descriptions: + - interface_id: 1/1 + description: new first Ansible interface test + - interface_id: 1/2 + description: second Ansible interface test + - interface_id: 1/3 + description: third Ansible interface test + register: nm_update_port_channel_interface_descriptions + + - name: Delete a port channel interface members descriptions + cisco.mso.ndo_port_channel_interface: &delete_port_channel_interface_desciptions + <<: *update_port_channel_interface_descriptions + interface_descriptions: [] + register: nm_delete_port_channel_interface_descriptions + + - name: Delete a port channel interface member and add descriptions again + cisco.mso.ndo_port_channel_interface: &delete_port_channel_interface_member + <<: *delete_port_channel_interface_desciptions + interfaces: 1/1-2 + interface_descriptions: + - interface_id: 1/1 + description: new first Ansible interface test + - interface_id: 1/2 + description: second Ansible interface test + register: nm_delete_port_channel_interface_member + + - name: Assert port channel interface update tasks + assert: + that: + - cm_update_port_channel_interface_node is changed + - cm_update_port_channel_interface_node.current.node == "102" + - nm_update_port_channel_interface_node is changed + - nm_update_port_channel_interface_node.current.node == "102" + - nm_update_port_channel_interface_node.current.interfaceDescriptions | length == 1 + - nm_update_port_channel_interface_node.current.interfaceDescriptions.0.nodeID == "102" + - nm_update_port_channel_interface_node.current.interfaceDescriptions.0.interfaceID == "1/1" + - nm_update_port_channel_interface_node.current.interfaceDescriptions.0.description == "first Ansible interface test" + - nm_update_port_channel_interface_node_again is not changed + - nm_update_port_channel_interface_node_again.current.node == "102" + - nm_update_port_channel_interface_node_again.current == nm_update_port_channel_interface_node_again.previous + - nm_update_node_without_previous_config is changed + - nm_update_node_without_previous_config.current.node == "103" + - nm_update_node_without_previous_config.current.interfaceDescriptions | length == 1 + - nm_update_node_without_previous_config.current.interfaceDescriptions.0.nodeID == "103" + - nm_update_node_without_previous_config.current.interfaceDescriptions.0.interfaceID == "1/1" + - nm_update_node_without_previous_config.current.interfaceDescriptions.0.description == "first Ansible interface test" + - nm_update_port_channel_interface_name is changed + - nm_update_port_channel_interface_name.current.name == "ansible_port_channel_interface_changed" + - nm_update_port_channel_interface_description is changed + - nm_update_port_channel_interface_description.current.description == "Ansible Port Channel test updated" + - nm_update_port_channel_interface_members is changed + - nm_update_port_channel_interface_members.current.memberInterfaces == "1/1-3,1/8-10,2/3-5" + - nm_update_port_channel_interface_members.current.interfaceDescriptions | length == 1 + - nm_update_port_channel_interface_members.current.interfaceDescriptions.0.nodeID == "103" + - nm_update_port_channel_interface_members.current.interfaceDescriptions.0.interfaceID == "1/1" + - nm_update_port_channel_interface_members.current.interfaceDescriptions.0.description == "first Ansible interface test" + - nm_update_port_channel_interface_descriptions is changed + - nm_update_port_channel_interface_descriptions.current.interfaceDescriptions | length == 3 + - nm_update_port_channel_interface_descriptions.current.interfaceDescriptions.0.nodeID == "103" + - nm_update_port_channel_interface_descriptions.current.interfaceDescriptions.0.interfaceID == "1/1" + - nm_update_port_channel_interface_descriptions.current.interfaceDescriptions.0.description == "new first Ansible interface test" + - nm_update_port_channel_interface_descriptions.current.interfaceDescriptions.1.nodeID == "103" + - nm_update_port_channel_interface_descriptions.current.interfaceDescriptions.1.interfaceID == "1/2" + - nm_update_port_channel_interface_descriptions.current.interfaceDescriptions.1.description == "second Ansible interface test" + - nm_update_port_channel_interface_descriptions.current.interfaceDescriptions.2.nodeID == "103" + - nm_update_port_channel_interface_descriptions.current.interfaceDescriptions.2.interfaceID == "1/3" + - nm_update_port_channel_interface_descriptions.current.interfaceDescriptions.2.description == "third Ansible interface test" + - nm_delete_port_channel_interface_descriptions is changed + - nm_delete_port_channel_interface_descriptions.current.interfaceDescriptions is not defined + - nm_delete_port_channel_interface_member is changed + - nm_delete_port_channel_interface_member.current.memberInterfaces == "1/1-2" + - nm_delete_port_channel_interface_member.current.interfaceDescriptions | length == 2 + - nm_delete_port_channel_interface_member.current.interfaceDescriptions.0.nodeID == "103" + - nm_delete_port_channel_interface_member.current.interfaceDescriptions.0.interfaceID == "1/1" + - nm_delete_port_channel_interface_member.current.interfaceDescriptions.0.description == "new first Ansible interface test" + - nm_delete_port_channel_interface_member.current.interfaceDescriptions.1.nodeID == "103" + - nm_delete_port_channel_interface_member.current.interfaceDescriptions.1.interfaceID == "1/2" + - nm_delete_port_channel_interface_member.current.interfaceDescriptions.1.description == "second Ansible interface test" + + # QUERY + + - name: Create another port channel interface + cisco.mso.ndo_port_channel_interface: &create_port_channel_interface_2 + <<: *mso_info + template: ansible_fabric_resource_template + port_channel_interface: ansible_port_channel_interface_2 + node: 101 + interfaces: 1/1 + interface_policy: Gaspard_Interface_setting_test + state: present + + - name: Query a port channel interface with template_name + cisco.mso.ndo_port_channel_interface: + <<: *create_port_channel_interface_2 + state: query + register: query_one + + - name: Query all port channel interfaces in the template + cisco.mso.ndo_port_channel_interface: + <<: *mso_info + template: ansible_fabric_resource_template + state: query + register: query_all + + - name: Assert port channel interface query tasks + assert: + that: + - query_one is not changed + - query_one.current.name == "ansible_port_channel_interface_2" + - query_all is not changed + - query_all.current | length == 2 + - query_all.current.0.name == "ansible_port_channel_interface_changed" + - query_all.current.0.name == "ansible_port_channel_interface_2" + + # DELETE + + - name: Delete a port channel interface (check_mode) + cisco.mso.ndo_port_channel_interface: &delete_port_channel_interface + <<: *delete_port_channel_interface_member + state: absent + check_mode: true + register: cm_delete_port_channel_interface + + - name: Delete a port channel interface + cisco.mso.ndo_port_channel_interface: + <<: *delete_port_channel_interface + register: nm_delete_port_channel_interface + + - name: Delete a port channel interface again + cisco.mso.ndo_port_channel_interface: + <<: *delete_port_channel_interface + register: nm_delete_port_channel_interface_again + + - name: Assert port channel interface deletion tasks + assert: + that: + - cm_delete_port_channel_interface is changed + - cm_delete_port_channel_interface.previous.name == "ansible_port_channel_interface_changed" + - cm_delete_port_channel_interface.current == {} + - nm_delete_port_channel_interface is changed + - nm_delete_port_channel_interface.previous.name == "ansible_port_channel_interface_changed" + - nm_delete_port_channel_interface.current == {} + - nm_delete_port_channel_interface_again is not changed + - nm_delete_port_channel_interface_again.previous == {} + - nm_delete_port_channel_interface_again.current == {} + + # CLEANUP TEMPLATE + + - name: Ensure templates do not exist + cisco.mso.ndo_template: + <<: *template_absent