From 8a0ef1a87a27f2f87f13d80eff01900bb02e2d6d Mon Sep 17 00:00:00 2001 From: Gaspard Micol Date: Fri, 11 Oct 2024 10:26:45 -0400 Subject: [PATCH] [ignore] Modify Documentation for ndo_port_channel_interface. Add flags to check valid interfaces and descriptions inputs. --- plugins/modules/ndo_port_channel_interface.py | 187 ++++++++++++------ .../ndo_port_channel_interface/tasks/main.yml | 149 ++++++++++++-- 2 files changed, 265 insertions(+), 71 deletions(-) diff --git a/plugins/modules/ndo_port_channel_interface.py b/plugins/modules/ndo_port_channel_interface.py index 7d6337c6..f4a00df6 100644 --- a/plugins/modules/ndo_port_channel_interface.py +++ b/plugins/modules/ndo_port_channel_interface.py @@ -50,9 +50,10 @@ description: - The node ID. type: str - member_interfaces: + interfaces: description: - The list of used Interface IDs. + - Ranges of Interface IDs can be used. type: list elements: str aliases: [ members ] @@ -74,7 +75,7 @@ suboptions: interface_id: description: - - The interface ID to which attach a description. + - The interface ID. type: str description: description: @@ -101,12 +102,17 @@ description: My Ansible Port Channel port_channel_interface: ansible_port_channel_interface node: 101 - member_interfaces: + interfaces: - 1/1 + - 1/10-11 interface_policy_uuid: ansible_port_channel_policy 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 - name: Query an Port Channel Interface with template_name @@ -117,14 +123,16 @@ template: ansible_fabric_resource_template port_channel_interface: ansible_port_channel_interface state: query + register: query_one -- name: Query all IPort Channel Interfaces in the template +- name: Query all Port Channel Interfaces in the 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 an Port Channel Interface cisco.mso.ndo_port_channel_interface: @@ -140,6 +148,7 @@ """ import copy +import re from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.mso.plugins.module_utils.mso import ( MSOModule, @@ -151,6 +160,26 @@ ) +def lookup_valid_interfaces(interfaces): + interface_ids = [] + errors_interfaces = [] + modified_interfaces = interfaces.replace(" ", "").split(",") + for interface in modified_interfaces: + if re.fullmatch(r"((\d+/)+\d+-\d+$)", interface): + slots = interface.rsplit("/", 1)[0] + range_start, range_stop = interface.rsplit("/", 1)[1].split("-") + if int(range_stop) > int(range_start): + for x in range(int(range_start), int(range_stop) + 1): + interface_ids.append("{0}/{1}".format(slots, x)) + else: + errors_interfaces.append(interface) + elif re.fullmatch(r"((\d+/)+\d+$)", interface): + interface_ids.append(interface) + else: + errors_interfaces.append(interface) + return set(interface_ids), errors_interfaces + + def main(): argument_spec = mso_argument_spec() argument_spec.update( @@ -159,7 +188,7 @@ def main(): port_channel_interface_uuid=dict(type="str", aliases=["uuid", "port_channel_uuid"]), description=dict(type="str"), node=dict(type="str"), - member_interfaces=dict(type="list", elements="str", aliases=["members"]), + interfaces=dict(type="list", elements="str", aliases=["members"]), interface_policy=dict(type="str", aliases=["policy", "pc_policy"]), interface_policy_uuid=dict(type="str", aliases=["policy_uuid", "pc_policy_uuid"]), interface_descriptions=dict( @@ -177,8 +206,8 @@ def main(): argument_spec=argument_spec, supports_check_mode=True, required_if=[ - ["state", "absent", ["port_channel_interface"]], - ["state", "present", ["port_channel_interface"]], + ["state", "absent", ["template", "port_channel_interface"]], + ["state", "present", ["template", "port_channel_interface"]], ], ) @@ -189,7 +218,7 @@ def main(): port_channel_interface_uuid = module.params.get("port_channel_interface_uuid") description = module.params.get("description") node = module.params.get("node") - member_interfaces = module.params.get("member_interfaces") + interfaces = ",".join(module.params.get("interfaces")) if module.params.get("interfaces") else None interface_policy = module.params.get("interface_policy") interface_policy_uuid = module.params.get("interface_policy_uuid") interface_descriptions = module.params.get("interface_descriptions") @@ -231,7 +260,7 @@ def main(): mso.fail_json(msg="No existing Port Channel policy") if match: - if match.details.get("name") != port_channel_interface: + if port_channel_interface and match.details.get("name") != port_channel_interface: ops.append( dict( op="replace", @@ -241,7 +270,7 @@ def main(): ) match.details["name"] = port_channel_interface - if match.details.get("description") != description: + if description and match.details.get("description") != description: ops.append( dict( op="replace", @@ -251,7 +280,8 @@ def main(): ) match.details["description"] = description - if match.details.get("node") != node: + node_changed = False + if node and match.details.get("node") != node: ops.append( dict( op="replace", @@ -260,18 +290,9 @@ def main(): ) ) match.details["node"] = node + node_changed = True - if match.details.get("memberInterfaces") != member_interfaces: - ops.append( - dict( - op="replace", - path="{0}/{1}/memberInterfaces".format(path, match.index), - value=",".join(member_interfaces), - ) - ) - match.details["memberInterfaces"] = member_interfaces - - if match.details.get("policy") != interface_policy_uuid: + if interface_policy_uuid and match.details.get("policy") != interface_policy_uuid: ops.append( dict( op="replace", @@ -281,47 +302,103 @@ def main(): ) match.details["policy"] = interface_policy_uuid - if interface_descriptions is not None and match.details.get("interfaceDescriptions") != interface_descriptions: - ops.append( - dict( - op="replace", - path="{0}/{1}/interfaceDescriptions".format(path, match.index), - value=[ - { - "nodeID": node, - "interfaceID": interface.get("interface_id"), - "description": interface.get("description"), - } - for interface in interface_descriptions - ], + if interfaces and match.details.get("memberInterfaces") != interfaces: + interface_ids, errors_interfaces = lookup_valid_interfaces(interfaces) + if errors_interfaces: + mso.fail_json(msg=("Invalid interface inputs, {0}".format(errors_interfaces))) + else: + ops.append( + dict( + op="replace", + path="{0}/{1}/memberInterfaces".format(path, match.index), + value=interfaces, + ) ) - ) - match.details["interfaceDescriptions"] = interface_descriptions + match.details["memberInterfaces"] = interfaces + else: + interface_ids, errors_interfaces = lookup_valid_interfaces(match.details.get("memberInterfaces")) + + if interface_descriptions or (node_changed and match.details.get("interfaceDescriptions")): + if node_changed and interface_descriptions is None: + interface_descriptions = [ + { + "nodeID": node, + "interfaceID": interface.get("interfaceID"), + "description": interface.get("description"), + } + for interface in match.details["interfaceDescriptions"] + ] + else: + interface_descriptions = [ + { + "nodeID": match.details["node"], + "interfaceID": interface.get("interface_id"), + "description": interface.get("description"), + } + for interface in interface_descriptions + ] + error_descriptions = [] + for interface_description in interface_descriptions: + if interface_description["interfaceID"] not in interface_ids: + error_descriptions.append(interface_description["interfaceID"]) + if error_descriptions: + mso.fail_json( + msg=("Interface IDs with description {0} not in list of current interfaces {1}".format(error_descriptions, list(interface_ids))) + ) + 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": port_channel_interface, - "description": description, + config = { "node": node, - "memberInterfaces": ",".join(member_interfaces), + "memberInterfaces": interfaces, "policy": interface_policy_uuid, } - - if interface_descriptions: - payload["interfaceDescriptions"] = [ - { - "nodeID": 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) + missing_required_attributes = [] + for attribute_name, attribute_value in config.items(): + if not attribute_value: + missing_required_attributes.append(attribute_name) + if missing_required_attributes: + mso.fail_json(msg=("Missing required attributes {0} for creating a Port Channel Interface".format(missing_required_attributes))) + else: + payload = {"name": port_channel_interface} | config + interface_ids, errors_interfaces = lookup_valid_interfaces(payload["memberInterfaces"]) + if errors_interfaces: + mso.fail_json(msg=("Invalid interface inputs, {0}".format(errors_interfaces))) + if description: + payload["description"] = description + if interface_descriptions: + error_descriptions = [] + for interface_description in interface_descriptions: + if interface_description["interface_id"] not in interface_ids: + error_descriptions.append(interface_description["interface_id"]) + if error_descriptions: + mso.fail_json( + msg=("Interface IDs with description {0} not in list of current interfaces {1}".format(error_descriptions, list(interface_ids))) + ) + payload["interfaceDescriptions"] = [ + { + "nodeID": 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: diff --git a/tests/integration/targets/ndo_port_channel_interface/tasks/main.yml b/tests/integration/targets/ndo_port_channel_interface/tasks/main.yml index cc2fe3dd..aa9fd3d1 100644 --- a/tests/integration/targets/ndo_port_channel_interface/tasks/main.yml +++ b/tests/integration/targets/ndo_port_channel_interface/tasks/main.yml @@ -67,7 +67,7 @@ port_channel_interface: ansible_port_channel_interface description: Ansible Port Channel test node: 101 - member_interfaces: 1/1 + interfaces: 1/1 interface_policy: Gaspard_Interface_setting_test interface_descriptions: - interface_id: 1/1 @@ -119,7 +119,60 @@ - 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: Create a new port channel interface with invalid interface + cisco.mso.ndo_port_channel_interface: + <<: *mso_info + template: ansible_fabric_resource_template + port_channel_interface: ansible_port_channel_interface_error + node: 101 + interfaces: + - error + - 1 + interface_policy: Gaspard_Interface_setting_test + interface_descriptions: + - interface_id: 1/1 + description: first Ansible interface test + state: present + ignore_errors: true + register: nm_create_interface_invalid_interface + + - name: Create a new port channel interface with invalid 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 + interface_policy: Gaspard_Interface_setting_test + interface_descriptions: + - interface_id: 1/1 + description: first Ansible interface test + - interface_id: 1/2 + description: second Ansible interface test + state: present + ignore_errors: true + register: nm_create_invalid_interface_descriptions + + - name: Assert port channel interface creation errors tasks + assert: + that: + - nm_create_missing_node.msg == "Missing required attributes ['node'] for creating a Port Channel Interface" + - nm_create_interface_invalid_interface.msg == "Invalid interface inputs, ['error', '1']" + - nm_create_invalid_interface_descriptions.msg == "Interface IDs with description ['1/2'] not in list of current interfaces ['1/1']" # UPDATE @@ -139,15 +192,24 @@ 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 + <<: *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 policy + - 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 @@ -162,7 +224,7 @@ - name: Update a port channel interface members cisco.mso.ndo_port_channel_interface: &update_port_channel_interface_members <<: *update_port_channel_interface_policy - member_interfaces: 1/1-3 + interfaces: 1/1-3 register: nm_update_port_channel_interface_members - name: Update a port channel interface members descriptions @@ -177,10 +239,16 @@ description: third Ansible interface test register: nm_update_port_channel_interface_descriptions - - name: Delete a port channel interface member - cisco.mso.ndo_port_channel_interface: &delete_port_channel_interface_member + - name: Delete a port channel interface members descriptions + cisco.mso.ndo_port_channel_interface: &delete_port_channel_interface_desciptions <<: *update_port_channel_interface_descriptions - member_interfaces: 1/1-2 + 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 @@ -202,6 +270,12 @@ - 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 @@ -209,36 +283,79 @@ - nm_update_port_channel_interface_members is changed - nm_update_port_channel_interface_members.current.memberInterfaces == "1/1-3" - nm_update_port_channel_interface_members.current.interfaceDescriptions | length == 1 - - nm_update_port_channel_interface_members.current.interfaceDescriptions.0.nodeID == "102" + - 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 == "102" + - 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 == "102" + - 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 == "102" + - 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 + - "'interfaceDescriptions' not in nm_delete_port_channel_interface_descriptions.current" - 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 == "102" + - 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 == "102" + - 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" - + + # UPDATE ERRORS + + - name: Udpate a port channel interface with invalid interface + cisco.mso.ndo_port_channel_interface: + <<: *mso_info + template: ansible_fabric_resource_template + port_channel_interface: ansible_port_channel_interface_changed + interfaces: + - error + - 1/2-1 + state: present + ignore_errors: true + register: nm_update_nterface_invalid_interface + + - name: Update a port channel interface with invalid interface descriptions + cisco.mso.ndo_port_channel_interface: + <<: *mso_info + template: ansible_fabric_resource_template + port_channel_interface: ansible_port_channel_interface_changed + interface_descriptions: + - interface_id: 1/1 + description: first Ansible interface test + - interface_id: 1/2 + description: second Ansible interface test + - interface_id: 1/3 + description: third Ansible interface test + state: present + ignore_errors: true + register: nm_update_invalid_interface_descriptions + + - name: Assert port channel interface creation errors tasks + assert: + that: + - nm_update_nterface_invalid_interface.msg == "Invalid interface inputs, ['error', '1/2-1']" + - nm_update_invalid_interface_descriptions.msg == "Interface IDs with description ['1/3'] not in list of current interfaces ['1/1', '1/2']" + # QUERY - name: Create another port channel interface cisco.mso.ndo_port_channel_interface: &create_port_channel_interface_2 - <<: *create_port_channel_interface + <<: *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: