diff --git a/plugins/module_utils/mso.py b/plugins/module_utils/mso.py index f66d4cbb..27aa6691 100644 --- a/plugins/module_utils/mso.py +++ b/plugins/module_utils/mso.py @@ -318,6 +318,40 @@ def write_file(module, url, dest, content, resp, tmpsrc=None): os.remove(tmpsrc) +def format_interface_descriptions(mso, interface_descriptions, node=None): + if interface_descriptions: + + def format_range_interfaces(format_dict): + ids = format_dict.get("interfaceID") + if re.fullmatch(r"((\d+/)+\d+$)", ids): + yield format_dict + elif re.fullmatch(r"((\d+/)+\d+-\d+$)", ids): + slots = ids.rsplit("/", 1)[0] + range_start, range_stop = ids.rsplit("/", 1)[1].split("-") + if int(range_stop) > int(range_start): + for x in range(int(range_start), int(range_stop) + 1): + copy_format_dict = deepcopy(format_dict) + copy_format_dict.update(interfaceID="{0}/{1}".format(slots, x)) + yield copy_format_dict + else: + mso.fail_json(msg="Range start is greater than or equal to range stop for range of IDs '{0}'".format(ids)) + else: + mso.fail_json(msg="Incorrect interface ID or range of IDs. Got '{0}'".format(ids)) + + return [ + item + for interface_description in interface_descriptions + for item in format_range_interfaces( + { + "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"), + } + ) + ] + return [] + + 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..e8bcbc32 --- /dev/null +++ b/plugins/modules/ndo_port_channel_interface.py @@ -0,0 +1,381 @@ +#!/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 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 + 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: + - The list of interface descriptions of the Port Channel Interface. + - Providing a new list of O(interface_descriptions) will completely + replace an existing one from the Port Channel Interface. + - Providing an empty list will remove the O(interface_descriptions=[]) + from the Port Channel Interface. + type: list + elements: dict + suboptions: + interface_id: + description: + - 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 or group of interfaces. + 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 +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""" +- 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 single Ansible Interface + - interface_id: 1/10-11 + description: My group of Ansible Interfaces + 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: Update a Port Channel Interface's interfaces and their descriptions + cisco.mso.ndo_port_channel_interface: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_resource_template + name: ansible_port_channel_interface_changed + interfaces: + - 1/1 + - 1/3 + - 1/5-7 + interface_descriptions: + - interface_id: 1/1 + description: My single unchanged Ansible Interface + - interface_id: 1/3 + description: My new single Ansible Interface + - interface_id: 1/5-7 + description: My new group of Ansible Interfaces + 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_name + +- 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_uuid + +- 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, +) +from ansible_collections.cisco.mso.plugins.module_utils.utils import append_update_ops_data + + +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", required=True), + template=dict(type="str", required=True), + ), + 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", required=True), + 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 isinstance(interfaces, list): + 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") + + existing_port_channel_interfaces = mso_template.template.get("fabricResourceTemplate", {}).get("template", {}).get("portChannels", []) + object_description = "Port Channel Interface" + + if state in ["query", "absent"] and not 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: + port_channel_attrs_path = "/fabricResourceTemplate/template/portChannels/{0}".format(match.index) + 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")) + + mso_values = dict( + name=name, + node=node, + memberInterfaces=interfaces, + policy=interface_policy_group_uuid, + description=description, + ) + + if mso.existing: + if node and interface_descriptions: + interface_descriptions = format_interface_descriptions(mso, interface_descriptions, node) + elif node and interface_descriptions is None and mso.existing.get("interfaceDescriptions"): + interface_descriptions = format_interface_descriptions(mso, mso.existing["interfaceDescriptions"], node) + elif node is None and interface_descriptions: + interface_descriptions = format_interface_descriptions(mso, interface_descriptions, mso.existing["node"]) + mso_values["interfaceDescriptions"] = interface_descriptions + + append_update_ops_data(ops, match.details, port_channel_attrs_path, mso_values) + mso.sanitize(match.details, collate=True) + + else: + if not node: + mso.fail_json(msg=("Missing parameter 'node' for creating a Port Channel Interface")) + mso_values["interfaceDescriptions"] = format_interface_descriptions(mso, interface_descriptions, node) + mso.sanitize(mso_values) + ops.append(dict(op="add", path="/fabricResourceTemplate/template/portChannels/-", value=mso.sent)) + + elif state == "absent": + if mso.existing: + ops.append(dict(op="remove", path=port_channel_attrs_path)) + + 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", []) or [] + 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..989d99f0 --- /dev/null +++ b/tests/integration/targets/ndo_port_channel_interface/tasks/main.yml @@ -0,0 +1,487 @@ +# 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 + + - 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 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: ansible_test_interface_policy_group_port_channel + template: ansible_fabric_policy_template + 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" + + # 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: ansible_test_interface_policy_group_port_channel_2 + template: ansible_fabric_policy_template + 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-2 + description: All Ansible interface tests + 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 == "All Ansible interface tests" + - 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 == "All Ansible interface tests" + + # 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_group: + name: ansible_test_interface_policy_group_port_channel_2 + template: ansible_fabric_policy_template + 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 a port channel with template_name and UUID + cisco.mso.ndo_tenant_custom_qos_policy: &query_port_channel_interface_uuid + <<: *create_port_channel_interface_2 + uuid: '{{ query_one.current.uuid }}' + name: '{{ fakevar | default(omit)}}' + state: query + register: query_one_uuid + + - 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_one_uuid is not changed + - query_one_uuid.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.1.name == "ansible_port_channel_interface_2" + + # 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_group: + name: ansible_test_interface_policy_group_port_channel + template: ansible_fabric_policy_template + state: present + ignore_errors: true + register: nm_create_missing_node + + - name: Create a new port channel interface without valid range IDs in interface descriptions + cisco.mso.ndo_port_channel_interface: + <<: *mso_info + template: ansible_fabric_resource_template + port_channel_interface: ansible_port_channel_interface_error + node: 101 + interfaces: 1/1-2 + interface_policy_group: + name: ansible_test_interface_policy_group_port_channel + template: ansible_fabric_policy_template + interface_descriptions: + - 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 port channel interface without valid IDs values in interface descriptions + cisco.mso.ndo_port_channel_interface: + <<: *mso_info + template: ansible_fabric_resource_template + port_channel_interface: ansible_port_channel_interface_error + node: 101 + interfaces: 1/1-2 + interface_policy_group: + name: ansible_test_interface_policy_group_port_channel + template: ansible_fabric_policy_template + interface_descriptions: + - 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 port channel interface without an existing interface policy group + cisco.mso.ndo_port_channel_interface: + <<: *mso_info + template: ansible_fabric_resource_template + port_channel_interface: ansible_port_channel_interface_error + node: 101 + interfaces: 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 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_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 + + - 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: Delete a port channel interface with UUID + cisco.mso.ndo_port_channel_interface: + <<: *query_port_channel_interface_uuid + state: absent + register: nm_delete_port_channel_interface_uuid + + - 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 == {} + - nm_delete_port_channel_interface_uuid is changed + - nm_delete_port_channel_interface_uuid.previous.name == "ansible_port_channel_interface_2" + - nm_delete_port_channel_interface_uuid.current == {} + + # ERRORS AND NO PORT CHANNEL INTERFACES FOUND + + - name: Query all port channel interfaces in the template when all are deleted + cisco.mso.ndo_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_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 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 == "Port Channel Interface with the UUID{{":"}} 'non-existing-uuid' not found" + + # CLEANUP TEMPLATE + + - 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