diff --git a/plugins/module_utils/constants.py b/plugins/module_utils/constants.py index 6ae883e7..a82ae1a6 100644 --- a/plugins/module_utils/constants.py +++ b/plugins/module_utils/constants.py @@ -18,6 +18,8 @@ YES_OR_NO_TO_BOOL_STRING_MAP = {"yes": "true", "no": "false", True: "yes", False: "no"} +ENABLED_OR_DISABLED_TO_BOOL_STRING_MAP = {"enabled": True, "disabled": False} + NDO_4_UNIQUE_IDENTIFIERS = ["templateID", "autoRouteTargetImport", "autoRouteTargetExport"] NDO_API_VERSION_FORMAT = "/mso/api/{api_version}" diff --git a/plugins/modules/ndo_synce_interface_policy.py b/plugins/modules/ndo_synce_interface_policy.py new file mode 100644 index 00000000..ef99148a --- /dev/null +++ b/plugins/modules/ndo_synce_interface_policy.py @@ -0,0 +1,284 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2024, Anvitha Jain (@anvjain) + +# 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_synce_interface_policy +short_description: Manage syncE Interface Policies on Cisco Nexus Dashboard Orchestrator (NDO). +description: +- Manage syncE Interface Policies on Cisco Nexus Dashboard Orchestrator (NDO). +- This module is only supported on ND v3.1 (NDO v4.3) and later. +author: +- Anvitha Jain (@anvjain) +options: + template: + description: + - The name of the template. + - The template must be a fabric policy template. + type: str + required: true + interface_policy: + description: + - The name of the syncE Interface Policy. + type: str + aliases: [ name ] + interface_policy_uuid: + description: + - The uuid of the syncE Interface Policy. + - This parameter is required when the O(interface_policy) needs to be updated. + type: str + aliases: [ uuid ] + description: + description: + - The description of the syncE Interface Policy. + type: str + admin_state: + description: + - The administrative state of the syncE Interface Policy. + - The default value is disabled. + type: str + choices: [ enabled, disabled ] + sync_state_msg: + description: + - The sync state message of the syncE Interface Policy. + - The default value is enabled. + type: str + choices: [ enabled, disabled ] + selection_input: + description: + - The selection input of the syncE Interface Policy. + - The default value is disabled. + type: str + choices: [ enabled, disabled ] + src_priority: + description: + - The source priority of the syncE Interface Policy. + - The value must be an integer between 1 and 254. + - The default value is 100. + type: int + wait_to_restore: + description: + - The delay before attempting to restore synchronization on a SyncE interface after a disruption. + - The value must be an integer between 0 and 12. + - The default value is 5. + type: int + 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 syncE interface policy + cisco.mso.ndo_synce_interface_policy: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_policy_template + interface_policy: ansible_test_interface_policy + admin_state: enabled + sync_state_msg: enabled + selection_input: enabled + src_priority: 100 + wait_to_restore: 5 + state: present + +- name: Query a syncE interface policy with interface_policy name + cisco.mso.ndo_synce_interface_policy: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_policy_template + interface_policy: ansible_test_interface_policy + state: query + register: query_one + +- name: Query all syncE interface policies in the template + cisco.mso.ndo_synce_interface_policy: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_policy_template + state: query + register: query_all + +- name: Query a syncE interface policy with interface_policy uuid + cisco.mso.ndo_synce_interface_policy: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_policy_template + interface_policy_uuid: uuid + state: query + register: query_one_by_uuid + +- name: Delete a syncE interface policy + cisco.mso.ndo_synce_interface_policy: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_fabric_policy_template + interface_policy: ansible_test_interface_policy + state: absent +""" + +RETURN = r""" +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec +from ansible_collections.cisco.mso.plugins.module_utils.template import MSOTemplate, KVPair +from ansible_collections.cisco.mso.plugins.module_utils.constants import ENABLED_OR_DISABLED_TO_BOOL_STRING_MAP +import copy + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + dict( + template=dict(type="str", required=True), + interface_policy=dict(type="str", aliases=["name"]), + interface_policy_uuid=dict(type="str", aliases=["uuid"]), + description=dict(type="str"), + admin_state=dict(type="str", choices=["enabled", "disabled"]), + sync_state_msg=dict(type="str", choices=["enabled", "disabled"]), + selection_input=dict(type="str", choices=["enabled", "disabled"]), + src_priority=dict(type="int"), + wait_to_restore=dict(type="int"), + state=dict(type="str", choices=["absent", "query", "present"], default="query"), + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "present", ["interface_policy"]], + ["state", "absent", ["interface_policy"]], + ], + ) + + mso = MSOModule(module) + + template = module.params.get("template") + interface_policy = module.params.get("interface_policy") + interface_policy_uuid = module.params.get("interface_policy_uuid") + description = module.params.get("description") + admin_state = module.params.get("admin_state") + sync_state_msg = module.params.get("sync_state_msg") + selection_input = module.params.get("selection_input") + src_priority = module.params.get("src_priority") + wait_to_restore = module.params.get("wait_to_restore") + state = module.params.get("state") + + ops = [] + match = None + + mso_template = MSOTemplate(mso, "fabric_policy", template) + mso_template.validate_template("fabricPolicy") + + path = "/fabricPolicyTemplate/template/syncEthIntfPolicies" + + existing_interface_policies = mso_template.template.get("fabricPolicyTemplate", {}).get("template", {}).get("syncEthIntfPolicies", []) + + if interface_policy: + object_description = "SyncE Interface Policy" + if interface_policy_uuid: + match = mso_template.get_object_by_uuid(object_description, existing_interface_policies, interface_policy_uuid) + else: + kv_list = [KVPair("name", interface_policy)] + match = mso_template.get_object_by_key_value_pairs(object_description, existing_interface_policies, kv_list) + if match: + mso.existing = mso.previous = copy.deepcopy(match.details) + else: + mso.existing = mso.previous = existing_interface_policies + + if state == "present": + + mso.existing = {} + + if match: + sync_state_msg_value = ENABLED_OR_DISABLED_TO_BOOL_STRING_MAP.get(sync_state_msg) + selection_input_value = ENABLED_OR_DISABLED_TO_BOOL_STRING_MAP.get(selection_input) + + if interface_policy and match.details.get("name") != interface_policy: + ops.append(dict(op="replace", path="{0}/{1}/name".format(path, match.index), value=interface_policy)) + match.details["name"] = interface_policy + + if description is not None and match.details.get("description") != description: + ops.append(dict(op="replace", path="{0}/{1}/description".format(path, match.index), value=description)) + match.details["description"] = description + + if admin_state and match.details.get("adminState") != admin_state: + ops.append(dict(op="replace", path="{0}/{1}/adminState".format(path, match.index), value=admin_state)) + match.details["adminState"] = admin_state + + if sync_state_msg and match.details.get("syncStateMsgEnabled") != sync_state_msg_value: + ops.append(dict(op="replace", path="{0}/{1}/syncStateMsgEnabled".format(path, match.index), value=sync_state_msg_value)) + match.details["syncStateMsgEnabled"] = sync_state_msg_value + + if selection_input and match.details.get("selectionInputEnabled") != selection_input_value: + ops.append(dict(op="replace", path="{0}/{1}/selectionInputEnabled".format(path, match.index), value=selection_input_value)) + match.details["selectionInputEnabled"] = selection_input_value + + if src_priority and match.details.get("srcPriority") != src_priority: + ops.append(dict(op="replace", path="{0}/{1}/srcPriority".format(path, match.index), value=src_priority)) + match.details["srcPriority"] = src_priority + + if wait_to_restore and match.details.get("waitToRestore") != wait_to_restore: + ops.append(dict(op="replace", path="{0}/{1}/waitToRestore".format(path, match.index), value=wait_to_restore)) + match.details["waitToRestore"] = wait_to_restore + + mso.sanitize(match.details) + + else: + + payload = {"name": interface_policy, "templateId": mso_template.template.get("templateId"), "schemaId": mso_template.template.get("schemaId")} + if description: + payload["description"] = description + if admin_state: + payload["adminState"] = admin_state + if sync_state_msg: + payload["syncStateMsgEnabled"] = ENABLED_OR_DISABLED_TO_BOOL_STRING_MAP.get(sync_state_msg) + if selection_input: + payload["selectionInputEnabled"] = ENABLED_OR_DISABLED_TO_BOOL_STRING_MAP.get(selection_input) + if src_priority: + payload["srcPriority"] = src_priority + if wait_to_restore: + payload["waitToRestore"] = wait_to_restore + + ops.append(dict(op="add", path="{0}/-".format(path), value=copy.deepcopy(payload))) + + mso.sanitize(payload) + + mso.existing = mso.proposed + + elif state == "absent": + if match: + ops.append(dict(op="remove", path="{0}/{1}".format(path, match.index))) + mso.existing = {} + + if not module.check_mode and ops: + mso.request(mso_template.template_path, method="PATCH", data=ops) + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/ndo_synce_interface_policy/aliases b/tests/integration/targets/ndo_synce_interface_policy/aliases new file mode 100644 index 00000000..5042c9c0 --- /dev/null +++ b/tests/integration/targets/ndo_synce_interface_policy/aliases @@ -0,0 +1,2 @@ +# No ACI MultiSite infrastructure, so not enabled +# unsupported diff --git a/tests/integration/targets/ndo_synce_interface_policy/tasks/main.yml b/tests/integration/targets/ndo_synce_interface_policy/tasks/main.yml new file mode 100644 index 00000000..e0e3a1db --- /dev/null +++ b/tests/integration/targets/ndo_synce_interface_policy/tasks/main.yml @@ -0,0 +1,306 @@ +# Test code for the MSO modules +# Copyright: (c) 2024, Anvitha Jain (@anvjain) + +# 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("debug") }}' + +# QUERY VERSION +- name: Query MSO version + cisco.mso.mso_version: + <<: *mso_info + state: query + register: version + + +- name: Execute tasks only for MSO version > 4.3 + when: version.current.version is version('4.3', '>=') + block: + - name: Remove fabric template + cisco.mso.ndo_template: &template_absent + <<: *mso_info + name: ansible_fabric_policy_template + type: fabric_policy + state: absent + + - name: Create a fabric template + cisco.mso.ndo_template: + <<: *mso_info + name: ansible_fabric_policy_template + type: fabric_policy + state: present + + # CREATE + - name: Create a new syncE interface policy (check mode) + cisco.mso.ndo_synce_interface_policy: &add_synce_interface_policy + <<: *mso_info + template: ansible_fabric_policy_template + interface_policy: ansible_test_synce_interface_policy + state: present + check_mode: true + register: cm_add_synce_interface_policy + + - name: Create a new syncE interface policy + cisco.mso.ndo_synce_interface_policy: + <<: *add_synce_interface_policy + register: nm_add_synce_interface_policy + + - name: Create a syncE interface policy again + cisco.mso.ndo_synce_interface_policy: + <<: *add_synce_interface_policy + register: nm_add_synce_interface_policy_again + + - name: Assert syncE interface policy was created + assert: + that: + - cm_add_synce_interface_policy is changed + - cm_add_synce_interface_policy.previous == {} + - cm_add_synce_interface_policy.current == nm_add_synce_interface_policy.proposed + - cm_add_synce_interface_policy.current.name == cm_add_synce_interface_policy.proposed.name == "ansible_test_synce_interface_policy" + - nm_add_synce_interface_policy is changed + - nm_add_synce_interface_policy.previous == {} + - nm_add_synce_interface_policy.current.name == "ansible_test_synce_interface_policy" + - nm_add_synce_interface_policy_again is not changed + - nm_add_synce_interface_policy_again.previous.name == nm_add_synce_interface_policy_again.current.name == "ansible_test_synce_interface_policy" + - nm_add_synce_interface_policy_again.previous.srcPriority == nm_add_synce_interface_policy_again.current.srcPriority == 100 + - nm_add_synce_interface_policy_again.previous.waitToRestore == nm_add_synce_interface_policy_again.current.waitToRestore == 5 + - nm_add_synce_interface_policy_again.previous.description == nm_add_synce_interface_policy_again.current.description == "" + - nm_add_synce_interface_policy_again.previous.uuid is defined + - nm_add_synce_interface_policy_again.current.uuid is defined + + # UPDATE + + - name: Update a syncE interface policy (check mode) + cisco.mso.ndo_synce_interface_policy: &update_synce_interface_policy + <<: *add_synce_interface_policy + description: changed_description + admin_state: enabled + sync_state_msg: disabled + selection_input: enabled + src_priority: 110 + wait_to_restore: 12 + state: present + check_mode: true + register: cm_update_synce_interface_policy + + - name: Update a syncE interface policy + cisco.mso.ndo_synce_interface_policy: + <<: *update_synce_interface_policy + register: nm_update_synce_interface_policy + + - name: Update a syncE interface policy again + cisco.mso.ndo_synce_interface_policy: + <<: *update_synce_interface_policy + register: nm_update_synce_interface_policy_again + + - name: Assert syncE interface policy was updated + assert: + that: + - cm_update_synce_interface_policy is changed + - cm_update_synce_interface_policy.previous.description == "" + - cm_update_synce_interface_policy.previous.adminState == "disabled" + - cm_update_synce_interface_policy.previous.syncStateMsgEnabled == true + - cm_update_synce_interface_policy.previous.selectionInputEnabled == false + - cm_update_synce_interface_policy.previous.srcPriority == 100 + - cm_update_synce_interface_policy.previous.waitToRestore == 5 + - cm_update_synce_interface_policy.current == cm_update_synce_interface_policy.proposed + - cm_update_synce_interface_policy.current.description == cm_update_synce_interface_policy.proposed.description == "changed_description" + - cm_update_synce_interface_policy.current.adminState == cm_update_synce_interface_policy.proposed.adminState == "enabled" + - cm_update_synce_interface_policy.current.syncStateMsgEnabled == cm_update_synce_interface_policy.proposed.syncStateMsgEnabled == false + - cm_update_synce_interface_policy.current.selectionInputEnabled == cm_update_synce_interface_policy.proposed.selectionInputEnabled == true + - cm_update_synce_interface_policy.current.srcPriority == cm_update_synce_interface_policy.proposed.srcPriority == 110 + - cm_update_synce_interface_policy.current.waitToRestore == cm_update_synce_interface_policy.proposed.waitToRestore == 12 + - nm_update_synce_interface_policy is changed + - nm_update_synce_interface_policy.current.description == "changed_description" + - nm_update_synce_interface_policy.current.adminState == "enabled" + - nm_update_synce_interface_policy.current.syncStateMsgEnabled == false + - nm_update_synce_interface_policy.current.selectionInputEnabled == true + - nm_update_synce_interface_policy.current.srcPriority == 110 + - nm_update_synce_interface_policy.current.waitToRestore == 12 + - nm_update_synce_interface_policy_again is not changed + - nm_update_synce_interface_policy_again.previous.description == nm_update_synce_interface_policy_again.current.description == "changed_description" + - nm_update_synce_interface_policy_again.previous.adminState == nm_update_synce_interface_policy_again.current.adminState == "enabled" + - nm_update_synce_interface_policy_again.previous.syncStateMsgEnabled == nm_update_synce_interface_policy_again.current.syncStateMsgEnabled == false + - nm_update_synce_interface_policy_again.previous.selectionInputEnabled == nm_update_synce_interface_policy_again.current.selectionInputEnabled == true + - nm_update_synce_interface_policy_again.previous.srcPriority == nm_update_synce_interface_policy_again.current.srcPriority == 110 + - nm_update_synce_interface_policy_again.previous.waitToRestore == nm_update_synce_interface_policy_again.current.waitToRestore == 12 + + - name: Update a syncE interface policy name + cisco.mso.ndo_synce_interface_policy: + <<: *update_synce_interface_policy + interface_policy_uuid: '{{ nm_update_synce_interface_policy.current.uuid }}' + interface_policy: ansible_test_synce_interface_policy_changed + register: nm_update_synce_interface_policy_name + + - name: Assert syncE interface policy was updated + assert: + that: + - nm_update_synce_interface_policy_name is changed + - nm_update_synce_interface_policy_name.previous.name == "ansible_test_synce_interface_policy" + - nm_update_synce_interface_policy_name.current.name == "ansible_test_synce_interface_policy_changed" + + # QUERY + + - name: Create another syncE interface policy + cisco.mso.ndo_synce_interface_policy: &add_synce_interface_policy_2 + <<: *mso_info + template: ansible_fabric_policy_template + interface_policy: ansible_test_synce_interface_policy_2 + description: "This is a test syncE interface policy" + admin_state: enabled + sync_state_msg: enabled + selection_input: enabled + src_priority: 250 + wait_to_restore: 1 + state: present + register: nm_add_synce_interface_policy_2 + + - name: Query a syncE interface policy with interface_policy name + cisco.mso.ndo_synce_interface_policy: + <<: *mso_info + template: ansible_fabric_policy_template + interface_policy: ansible_test_synce_interface_policy_2 + state: query + register: query_one + + - name: Query all syncE interface policies in the template + cisco.mso.ndo_synce_interface_policy: + <<: *mso_info + template: ansible_fabric_policy_template + state: query + register: query_all + + - name: Verify query_one and query_all + assert: + that: + - query_all is not changed + - query_one is not changed + - query_all.current | length >= 2 + - query_one.current.name == "ansible_test_synce_interface_policy_2" + - query_one.current.description == "This is a test syncE interface policy" + + - name: Query a syncE interface policy with interface_policy uuid + cisco.mso.ndo_synce_interface_policy: + <<: *mso_info + template: ansible_fabric_policy_template + interface_policy_uuid: '{{ nm_update_synce_interface_policy.current.uuid }}' + interface_policy: ansible_test_synce_interface_policy_changed + state: query + register: query_one_uuid + + - name: Verify query_one_uuid + assert: + that: + - query_one_uuid is not changed + - query_one_uuid.current.name == "ansible_test_synce_interface_policy_changed" + + # DELETE + + - name: Delete a syncE interface policy (check mode) + cisco.mso.ndo_synce_interface_policy: &rm_synce_interface_policy + <<: *mso_info + template: ansible_fabric_policy_template + interface_policy: ansible_test_synce_interface_policy_changed + state: absent + check_mode: true + register: cm_rm_synce_interface_policy + + - name: Delete a syncE interface policy + cisco.mso.ndo_synce_interface_policy: + <<: *rm_synce_interface_policy + register: nm_rm_synce_interface_policy + + - name: Delete a syncE interface policy again + cisco.mso.ndo_synce_interface_policy: + <<: *rm_synce_interface_policy + register: nm_rm_synce_interface_policy_again + + - name: Assert syncE interface policy was deleted + assert: + that: + - cm_rm_synce_interface_policy is changed + - cm_rm_synce_interface_policy.previous.name == "ansible_test_synce_interface_policy_changed" + - cm_rm_synce_interface_policy.previous.description == "changed_description" + - cm_rm_synce_interface_policy.current == {} + - nm_rm_synce_interface_policy is changed + - nm_rm_synce_interface_policy.previous.name == "ansible_test_synce_interface_policy_changed" + - nm_rm_synce_interface_policy.previous.description == "changed_description" + - nm_rm_synce_interface_policy.current == {} + - nm_rm_synce_interface_policy_again is not changed + - nm_rm_synce_interface_policy_again.previous == nm_rm_synce_interface_policy_again.current == {} + + # ERRORS + + - name: Error - admin_state set to invalid value + cisco.mso.ndo_synce_interface_policy: + <<: *add_synce_interface_policy_2 + admin_state: invalid + state: present + register: error_admin_state + ignore_errors: true + + - name: Error - sync_state_msg set to invalid value + cisco.mso.ndo_synce_interface_policy: + <<: *add_synce_interface_policy_2 + sync_state_msg: invalid + register: error_sync_state_msg + ignore_errors: true + + - name: Error - selection_input set to invalid value + cisco.mso.ndo_synce_interface_policy: + <<: *add_synce_interface_policy_2 + selection_input: invalid + state: present + register: error_selection_input + ignore_errors: true + + - name: Error - src_priority set to invalid value + cisco.mso.ndo_synce_interface_policy: + <<: *add_synce_interface_policy_2 + src_priority: 256 + state: present + register: error_src_priority + ignore_errors: true + + - name: Error - wait_to_restore set to invalid value + cisco.mso.ndo_synce_interface_policy: + <<: *add_synce_interface_policy_2 + wait_to_restore: 256 + state: present + register: error_wait_to_restore + ignore_errors: true + + - name: Assert errors + assert: + that: + - error_admin_state is failed + - error_admin_state.msg == "value of admin_state must be one of{{':'}} enabled, disabled, got{{':'}} invalid" + - error_sync_state_msg is failed + - error_sync_state_msg.msg == "value of sync_state_msg must be one of{{':'}} enabled, disabled, got{{':'}} invalid" + - error_selection_input is failed + - error_selection_input.msg == "value of selection_input must be one of{{':'}} enabled, disabled, got{{':'}} invalid" + - error_src_priority is failed + - error_src_priority.msg == "MSO Error 400{{':'}} 'SrcPriority' allowed minimum value is 1 maximum value is 254" + - error_wait_to_restore is failed + - error_wait_to_restore.msg == "MSO Error 400{{':'}} 'WaitToRestore' allowed maximum value is 12" + + # CLEANUP TEMPLATE + + - name: Ensure templates do not exist + cisco.mso.ndo_template: + <<: *template_absent \ No newline at end of file