diff --git a/plugins/module_utils/mso.py b/plugins/module_utils/mso.py index ad95d039..27aa6691 100644 --- a/plugins/module_utils/mso.py +++ b/plugins/module_utils/mso.py @@ -318,19 +318,38 @@ def write_file(module, url, dest, content, resp, tmpsrc=None): os.remove(tmpsrc) -def format_interface_descriptions(interface_descriptions, node=None): +def format_interface_descriptions(mso, 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"), - } + + 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"), + } + ) ] - else: - formated_interface_descriptions = [] - return formated_interface_descriptions + return [] class MSOModule(object): diff --git a/plugins/modules/ndo_port_channel_interface.py b/plugins/modules/ndo_port_channel_interface.py index 3869701a..b076c085 100644 --- a/plugins/modules/ndo_port_channel_interface.py +++ b/plugins/modules/ndo_port_channel_interface.py @@ -78,10 +78,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: @@ -91,8 +93,10 @@ suboptions: 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. @@ -106,6 +110,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""" @@ -126,11 +138,9 @@ 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 + description: My single Ansible Interface + - interface_id: 1/10-11 + description: My group of Ansible Interfaces state: present register: port_channel_interface_1 @@ -152,7 +162,7 @@ template: ansible_fabric_resource_template name: ansible_port_channel_interface_changed state: query - register: query_one + register: query_name - name: Query a Port Channel Interface with UUID cisco.mso.ndo_port_channel_interface: @@ -162,7 +172,7 @@ template: ansible_fabric_resource_template uuid: "{{ port_channel_interface_1.current.uuid }}" state: query - register: query_one + register: query_uuid - name: Query all Port Channel Interfaces in a Fabric Resource Template cisco.mso.ndo_port_channel_interface: @@ -220,8 +230,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"], ), @@ -230,7 +240,7 @@ def main(): type="list", elements="dict", options=dict( - interface_id=dict(type="str"), + interface_id=dict(type="str", required=True), description=dict(type="str"), ), ), @@ -254,7 +264,7 @@ def main(): description = module.params.get("description") node = module.params.get("node") interfaces = module.params.get("interfaces") - if 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") @@ -319,9 +329,9 @@ def main(): 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) + interface_descriptions = format_interface_descriptions(mso, mso.existing["interfaceDescriptions"], node) else: - interface_descriptions = format_interface_descriptions(interface_descriptions, proposed_payload["node"]) + interface_descriptions = format_interface_descriptions(mso, 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 @@ -333,14 +343,14 @@ def main(): else: if not node: - mso.fail_json(msg=("ERROR: Missing parameter 'node' for creating a Port Channel Interface")) + mso.fail_json(msg=("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), + "interfaceDescriptions": format_interface_descriptions(mso, interface_descriptions, node), } mso.sanitize(payload) 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 810499b3..8cc890d8 100644 --- a/tests/integration/targets/ndo_port_channel_interface/tasks/main.yml +++ b/tests/integration/targets/ndo_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 port channel interface (check_mode) @@ -69,8 +92,8 @@ node: 101 interfaces: 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: - interface_id: 1/1 description: first Ansible interface test @@ -122,24 +145,6 @@ - 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) @@ -184,9 +189,9 @@ - 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 + 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 @@ -221,10 +226,8 @@ <<: *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 + - interface_id: 1/1-2 + description: All Ansible interface tests register: nm_delete_port_channel_interface_member - name: Assert port channel interface update tasks @@ -275,10 +278,10 @@ - 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.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 == "second Ansible interface test" + - nm_delete_port_channel_interface_member.current.interfaceDescriptions.1.description == "All Ansible interface tests" # QUERY @@ -289,7 +292,9 @@ port_channel_interface: ansible_port_channel_interface_2 node: 101 interfaces: 1/1 - interface_policy: Gaspard_Interface_setting_test + 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 @@ -313,7 +318,86 @@ - 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" + - 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_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 @@ -346,9 +430,39 @@ - nm_delete_port_channel_interface_again is not changed - nm_delete_port_channel_interface_again.previous == {} - nm_delete_port_channel_interface_again.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 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