diff --git a/plugins/module_utils/constants.py b/plugins/module_utils/constants.py index a82ae1a6..2e24247b 100644 --- a/plugins/module_utils/constants.py +++ b/plugins/module_utils/constants.py @@ -25,6 +25,18 @@ NDO_API_VERSION_FORMAT = "/mso/api/{api_version}" NDO_API_VERSION_PATH_FORMAT = "/mso/api/{api_version}/{path}" +NDO_CIPHER_SUITE_MAP = { + "128_gcm_aes": "128GcmAes", + "128_gcm_aes_xpn": "128GcmAesXpn", + "256_gcm_aes": "256GcmAes", + "256_gcm_aes_xpn": "256GcmAesXpn", +} + +NDO_SECURITY_POLICY_MAP = { + "should_secure": "shouldSecure", + "must_secure": "mustSecure", +} + EPG_U_SEG_ATTR_TYPE_MAP = { "ip": "ip", "mac": "mac", diff --git a/plugins/module_utils/mso.py b/plugins/module_utils/mso.py index 0a916421..f66d4cbb 100644 --- a/plugins/module_utils/mso.py +++ b/plugins/module_utils/mso.py @@ -1602,6 +1602,14 @@ def nd_request(self, path, method=None, data=None, file=None, qs=None, prefix="" self.fail_json(msg=msg) return {} + def verify_time_format(self, date_time): + if date_time != "now" or date_time != "infinite": + try: + formatted_date_time = datetime.datetime.strptime(date_time, "%Y-%m-%d %H:%M:%S") + return str(formatted_date_time) + except ValueError: + return self.fail_json(msg="ERROR: The time must be in 'YYYY-MM-DD HH:MM:SS' format.") + def service_node_ref_str_to_dict(serviceNodeRefStr): serviceNodeRefTokens = serviceNodeRefStr.split("/") diff --git a/plugins/modules/ndo_macsec_policy.py b/plugins/modules/ndo_macsec_policy.py new file mode 100644 index 00000000..500c0077 --- /dev/null +++ b/plugins/modules/ndo_macsec_policy.py @@ -0,0 +1,443 @@ +#!/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_macsec_policy +short_description: Manage MACsec Policies on Cisco Nexus Dashboard Orchestrator (NDO). +description: +- Manage MACsec 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 + macsec_policy: + description: + - The name of the MACsec Policy. + type: str + aliases: [ name ] + macsec_policy_uuid: + description: + - The UUID of the MACsec Policy. + - This parameter is required when the O(macsec_policy) needs to be updated. + type: str + aliases: [ uuid ] + description: + description: + - The description of the MACsec Policy. + type: str + admin_state: + description: + - The administrative state of the MACsec Policy. (Enables or disables the policy) + - The default value is C(enabled). + type: str + choices: [ enabled, disabled ] + interface_type: + description: + - The type of the interfaces this policy will be applied to. + type: str + choices: [ fabric, access ] + default: fabric + cipher_suite: + description: + - The cipher suite to be used for encryption. + - The default value is C(256_gcm_aes_xpn). + type: str + choices: [ 128_gcm_aes, 128_gcm_aes_xpn, 256_gcm_aes, 256_gcm_aes_xpn ] + window_size: + description: + - The window size defines the maximum number of frames that can be received out of order + - before a replay attack is detected. + - The value must be between 0 and 4294967295. + - The default value is 0 for type C(fabric) and 64 for type C(access). + type: int + security_policy: + description: + - The security policy to allow traffic on the link for the MACsec Policy. + - The default value is C(should_secure). + type: str + choices: [ should_secure, must_secure ] + sak_expiry_time: + description: + - The expiry time for the Security Association Key (SAK) for the MACsec Policy. + - The value must be 0 or between 60 and 2592000. + - The default value is 0. + type: int + confidentiality_offset: + description: + - The confidentiality offset for the MACsec Policy. + - The default value is 0. + - This parameter is only available for type C(access). + type: int + choices: [ 0, 30, 50 ] + key_server_priority: + description: + - The key server priority for the MACsec Policy. + - The value must be between 0 and 255. + - The default value 16 for type C(access). + - This parameter is only available for type C(access). + type: int + macsec_keys: + description: + - List of the MACsec Keys. + - Providing an empty list will remove the O(macsec_keys) from the MACsec Policy. + - The old O(macsec_keys) entries will be replaced with the new entries during update. + type: list + elements: dict + suboptions: + key_name: + description: + - The name of the MACsec Key. + - Key Name has to be Hex chars [0-9a-fA-F] + type: str + required: true + psk: + description: + - The Pre-Shared Key (PSK) for the MACsec Key. + - PSK has to be 64 chars long if cipher suite is C(256_gcm_aes) or C(256_gcm_aes_xpn). + - PSK has to be 32 chars long if cipher suite is C(128_gcm_aes) or C(128_gcm_aes_xpn). + - PSK has to be Hex chars [0-9a-fA-F] + type: str + required: true + start_time: + description: + - The start time for the MACsec Key. + - The date time format - YYYY-MM-DD HH:MM:SS or 'now' + - The start time for each key_name should be unique. + - The default value is C(now). + type: str + end_time: + description: + - The end time for the MACsec Key. + - The date time format - YYYY-MM-DD HH:MM:SS or 'infinite' + - The default value is C(infinite). + type: str + state: + description: + - Use C(absent) for removing. + - Use C(query) for listing an object or multiple objects. + - Use C(present) for creating or updating. + type: str + choices: [ absent, query, present ] + default: query +extends_documentation_fragment: cisco.mso.modules +""" + +EXAMPLES = r""" +- name: Create a new MACsec Policy of interface_type fabric + cisco.mso.ndo_macsec_policy: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_test_template + macsec_policy: ansible_test_macsec_policy + description: "Ansible Test MACsec Policy" + state: present + +- name: Create a new MACsec Policy of interface_type access + cisco.mso.ndo_macsec_policy: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_test_template + macsec_policy: ansible_test_macsec_policy + description: "Ansible Test MACsec Policy" + macsec_keys: + - key_name: ansible_test_key + psk: 'AA111111111111111111111111111111111111111111111111111111111111aa' + start_time: '2029-12-11 11:12:13' + end_time: 'infinite' + state: present + +- name: Query a MACsec Policy with macsec_policy name + cisco.mso.ndo_macsec_policy: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_test_template + macsec_policy: ansible_test_macsec_policy + state: query + register: query_one + +- name: Query all MACsec Policies + cisco.mso.ndo_macsec_policy: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_test_template + state: query + register: query_all + +- name: Query a MACsec Policy with macsec_policy UUID + cisco.mso.ndo_macsec_policy: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_test_template + macsec_policy_uuid: ansible_test_macsec_policy_uuid + state: query + register: query_uuid + +- name: Delete a MACsec Policy with name + cisco.mso.ndo_macsec_policy: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_test_template + macsec_policy: ansible_test_macsec_policy + state: absent + +- name: Delete a MACsec Policy with UUID + cisco.mso.ndo_macsec_policy: + host: mso_host + username: admin + password: SomeSecretPassword + template: ansible_test_template + macsec_policy_uuid: ansible_test_macsec_policy_uuid + 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 NDO_CIPHER_SUITE_MAP, NDO_SECURITY_POLICY_MAP +import copy + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + dict( + template=dict(type="str", required=True), + macsec_policy=dict(type="str", aliases=["name"]), + macsec_policy_uuid=dict(type="str", aliases=["uuid"]), + description=dict(type="str"), + admin_state=dict(type="str", choices=["enabled", "disabled"]), + interface_type=dict(type="str", choices=["fabric", "access"], default="fabric"), + cipher_suite=dict(type="str", choices=list(NDO_CIPHER_SUITE_MAP)), + window_size=dict(type="int"), + security_policy=dict(type="str", choices=list(NDO_SECURITY_POLICY_MAP)), + sak_expiry_time=dict(type="int"), + confidentiality_offset=dict(type="int", choices=[0, 30, 50]), + key_server_priority=dict(type="int"), + macsec_keys=dict( + type="list", + elements="dict", + options=dict( + key_name=dict(type="str", required=True), + psk=dict(type="str", required=True, no_log=True), + start_time=dict(type="str"), + end_time=dict(type="str"), + ), + no_log=False, + ), + state=dict(type="str", choices=["absent", "query", "present"], default="query"), + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "present", ["macsec_policy", "macsec_policy_uuid"], True], + ["state", "absent", ["macsec_policy", "macsec_policy_uuid"], True], + ], + ) + + mso = MSOModule(module) + + template = module.params.get("template") + macsec_policy = module.params.get("macsec_policy") + macsec_policy_uuid = module.params.get("macsec_policy_uuid") + description = module.params.get("description") + admin_state = module.params.get("admin_state") + interface_type = module.params.get("interface_type") + cipher_suite = NDO_CIPHER_SUITE_MAP.get(module.params.get("cipher_suite")) + window_size = module.params.get("window_size") + security_policy = NDO_SECURITY_POLICY_MAP.get(module.params.get("security_policy")) + sak_expiry_time = module.params.get("sak_expiry_time") + confidentiality_offset = module.params.get("confidentiality_offset") + key_server_priority = module.params.get("key_server_priority") + macsec_keys = module.params.get("macsec_keys") + state = module.params.get("state") + + ops = [] + match = None + + mso_template = MSOTemplate(mso, "fabric_policy", template) + mso_template.validate_template("fabricPolicy") + + path = "/fabricPolicyTemplate/template/macsecPolicies" + object_description = "MACsec Policy" + + existing_macsec_policies = mso_template.template.get("fabricPolicyTemplate", {}).get("template", {}).get("macsecPolicies", []) + if macsec_policy or macsec_policy_uuid: + match = mso_template.get_object_by_key_value_pairs( + object_description, + existing_macsec_policies, + [KVPair("uuid", macsec_policy_uuid) if macsec_policy_uuid else KVPair("name", macsec_policy)], + ) + if match: + mso.existing = mso.previous = copy.deepcopy(match.details) + else: + mso.existing = mso.previous = existing_macsec_policies + + if state == "present": + + if match: + + if macsec_policy and match.details.get("name") != macsec_policy: + ops.append(dict(op="replace", path="{0}/{1}/name".format(path, match.index), value=macsec_policy)) + match.details["name"] = macsec_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 interface_type and match.details.get("type") != interface_type: + mso.fail_json(msg="Type cannot be changed for an existing MACsec Policy.") + + if cipher_suite and match.details.get("macsecParams")["cipherSuite"] != cipher_suite: + ops.append(dict(op="replace", path="{0}/{1}/macsecParams/cipherSuite".format(path, match.index), value=cipher_suite)) + match.details["macsecParams"]["cipherSuite"] = cipher_suite + + if window_size and match.details.get("macsecParams")["windowSize"] != window_size: + ops.append(dict(op="replace", path="{0}/{1}/macsecParams/windowSize".format(path, match.index), value=window_size)) + match.details["macsecParams"]["windowSize"] = window_size + + if security_policy and match.details.get("macsecParams")["securityPol"] != security_policy: + ops.append(dict(op="replace", path="{0}/{1}/macsecParams/securityPol".format(path, match.index), value=security_policy)) + match.details["macsecParams"]["securityPol"] = security_policy + + if sak_expiry_time and match.details.get("macsecParams")["sakExpiryTime"] != sak_expiry_time: + ops.append(dict(op="replace", path="{0}/{1}/macsecParams/sakExpiryTime".format(path, match.index), value=sak_expiry_time)) + match.details["macsecParams"]["sakExpiryTime"] = sak_expiry_time + + if interface_type == "access": + if confidentiality_offset and match.details.get("macsecParams")["confOffSet"] != confidentiality_offset: + ops.append( + dict(op="replace", path="{0}/{1}/macsecParams/confOffSet".format(path, match.index), value="offset{0}".format(confidentiality_offset)) + ) + match.details["macsecParams"]["confOffSet"] = "offset{0}".format(confidentiality_offset) + + if key_server_priority and match.details.get("macsecParams")["keyServerPrio"] != key_server_priority: + ops.append(dict(op="replace", path="{0}/{1}/macsecParams/keyServerPrio".format(path, match.index), value=key_server_priority)) + match.details["macsecParams"]["keyServerPrio"] = key_server_priority + + if macsec_keys: + # updating macsec_keys modifies the existing list with the new list + macsec_keys_list = [] + for macsec_key in macsec_keys: + macsec_keys_list.append( + dict( + keyname=macsec_key.get("key_name"), + psk=macsec_key.get("psk"), + start=mso.verify_time_format(macsec_key.get("start_time")) if macsec_key.get("start_time") else None, + end=mso.verify_time_format(macsec_key.get("end_time")) if macsec_key.get("end_time") else None, + ) + ) + + if macsec_keys_list != match.details.get("macsecKeys", []): + ops.append(dict(op="replace", path="{0}/{1}/macsecKeys".format(path, match.index), value=macsec_keys_list)) + match.details["macsecKeys"] = macsec_keys + elif macsec_keys == []: + # remove macsec_keys if the list is empty + ops.append(dict(op="remove", path="{0}/{1}/macsecKeys".format(path, match.index))) + match.details.pop("macsecKeys", None) + + mso.sanitize(match.details) + + else: + macsec_param_map = {} + + payload = {"name": macsec_policy, "templateId": mso_template.template.get("templateId"), "schemaId": mso_template.template.get("schemaId")} + payload["type"] = interface_type + + if description: + payload["description"] = description + if admin_state: + payload["adminState"] = admin_state + if cipher_suite: + macsec_param_map["cipherSuite"] = cipher_suite + if window_size: + macsec_param_map["windowSize"] = window_size + if security_policy: + macsec_param_map["securityPol"] = security_policy + if sak_expiry_time: + macsec_param_map["sakExpiryTime"] = sak_expiry_time + + if interface_type == "access": + if confidentiality_offset: + macsec_param_map["confOffSet"] = "offset{0}".format(confidentiality_offset) + if key_server_priority: + macsec_param_map["keyServerPrio"] = key_server_priority + payload["macsecParams"] = macsec_param_map + + if macsec_keys: + macsec_keys_list = [] + for macsec_key in macsec_keys: + macsec_key_dict = { + "keyname": macsec_key.get("key_name"), + "psk": macsec_key.get("psk"), + } + if macsec_key.get("start_time"): + macsec_key_dict["start"] = macsec_key.get("start_time") + if macsec_key.get("end_time"): + macsec_key_dict["end"] = macsec_key.get("end_time") + macsec_keys_list.append(macsec_key_dict) + payload["macsecKeys"] = macsec_keys_list + + 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))) + + if not module.check_mode and ops: + response = mso.request(mso_template.template_path, method="PATCH", data=ops) + macsec_policies = response.get("fabricPolicyTemplate", {}).get("template", {}).get("macsecPolicies", []) + match = mso_template.get_object_by_key_value_pairs( + object_description, + macsec_policies, + [KVPair("uuid", macsec_policy_uuid) if macsec_policy_uuid else KVPair("name", macsec_policy)], + ) + 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_macsec_policy/aliases b/tests/integration/targets/ndo_macsec_policy/aliases new file mode 100644 index 00000000..5042c9c0 --- /dev/null +++ b/tests/integration/targets/ndo_macsec_policy/aliases @@ -0,0 +1,2 @@ +# No ACI MultiSite infrastructure, so not enabled +# unsupported diff --git a/tests/integration/targets/ndo_macsec_policy/tasks/main.yml b/tests/integration/targets/ndo_macsec_policy/tasks/main.yml new file mode 100644 index 00000000..4482c187 --- /dev/null +++ b/tests/integration/targets/ndo_macsec_policy/tasks/main.yml @@ -0,0 +1,401 @@ +# 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: + <<: *template_absent + state: present + + # CREATE + + # MACsec policy interface_type fabric + - name: Create a MACsec policy of interface_type 'fabric' (check mode) + cisco.mso.ndo_macsec_policy: &add_macsec_policy + <<: *mso_info + template: ansible_fabric_policy_template + macsec_policy: ansible_macsec_policy + state: present + check_mode: true + register: cm_add_macsec_policy + + - name: Create a MACsec policy of interface_type 'fabric' + cisco.mso.ndo_macsec_policy: + <<: *add_macsec_policy + register: nm_add_macsec_policy + + - name: Create MACsec policy again + cisco.mso.ndo_macsec_policy: + <<: *add_macsec_policy + register: nm_add_macsec_policy_again + + - name: Assert that the MACsec policy was created + assert: + that: + - cm_add_macsec_policy is changed + - cm_add_macsec_policy.previous == nm_add_macsec_policy.previous == {} + - cm_add_macsec_policy.current.name == cm_add_macsec_policy.proposed.name == 'ansible_macsec_policy' + - cm_add_macsec_policy.current.type == cm_add_macsec_policy.proposed.type == 'fabric' + - nm_add_macsec_policy is changed + - nm_add_macsec_policy.current.name == 'ansible_macsec_policy' + - nm_add_macsec_policy.current.type == 'fabric' + - nm_add_macsec_policy.current.description == '' + - nm_add_macsec_policy.current.adminState == 'enabled' + - nm_add_macsec_policy.current.macsecParams.cipherSuite == '256GcmAesXpn' + - nm_add_macsec_policy.current.macsecParams.sakExpiryTime == 0 + - nm_add_macsec_policy.current.macsecParams.securityPol == 'shouldSecure' + - nm_add_macsec_policy.current.macsecParams.windowSize == 0 + - nm_add_macsec_policy.current.uuid is defined + - nm_add_macsec_policy_again is not changed + - nm_add_macsec_policy_again.previous.name == nm_add_macsec_policy_again.current.name == 'ansible_macsec_policy' + - nm_add_macsec_policy_again.previous.type == nm_add_macsec_policy_again.current.type == 'fabric' + - nm_add_macsec_policy_again.previous.description == nm_add_macsec_policy_again.current.description == '' + - nm_add_macsec_policy_again.previous.uuid is defined + - nm_add_macsec_policy_again.current.uuid is defined + - nm_add_macsec_policy_again.previous.macsecParams.cipherSuite == nm_add_macsec_policy_again.current.macsecParams.cipherSuite == '256GcmAesXpn' + - nm_add_macsec_policy_again.previous.macsecParams.sakExpiryTime == nm_add_macsec_policy_again.current.macsecParams.sakExpiryTime == 0 + - nm_add_macsec_policy_again.previous.macsecParams.securityPol == nm_add_macsec_policy_again.current.macsecParams.securityPol == 'shouldSecure' + - nm_add_macsec_policy_again.previous.macsecParams.windowSize == nm_add_macsec_policy_again.current.macsecParams.windowSize == 0 + + # MACsec policy interface_type access + - name: Create a MACsec policy of interface_type 'access' + cisco.mso.ndo_macsec_policy: &add_macsec_policy_access + <<: *mso_info + template: ansible_fabric_policy_template + macsec_policy: ansible_macsec_policy_access + interface_type: access + state: present + register: add_macsec_policy_access + + - name: Assert that the MACsec policy was created + assert: + that: + - add_macsec_policy_access is changed + - add_macsec_policy_access.previous == {} + - add_macsec_policy_access.current.name == 'ansible_macsec_policy_access' + - add_macsec_policy_access.current.type == 'access' + - add_macsec_policy_access.current.description == '' + - add_macsec_policy_access.current.adminState == 'enabled' + - add_macsec_policy_access.current.macsecParams.cipherSuite == '256GcmAesXpn' + - add_macsec_policy_access.current.macsecParams.sakExpiryTime == 0 + - add_macsec_policy_access.current.macsecParams.securityPol == 'shouldSecure' + - add_macsec_policy_access.current.macsecParams.windowSize == 64 + - add_macsec_policy_access.current.macsecParams.confOffSet == 'offset0' + - add_macsec_policy_access.current.macsecParams.keyServerPrio == 16 + - add_macsec_policy_access.current.uuid is defined + + # UPDATE + + # Only one macsec_keys can be added during creation + - name: Create another MACsec policy of interface_type 'access' + cisco.mso.ndo_macsec_policy: &add_macsec_policy_2 + <<: *mso_info + template: ansible_fabric_policy_template + macsec_policy: ansible_macsec_policy_2 + description: 'Ansible MACsec Policy description' + admin_state: 'enabled' + cipher_suite: '256_gcm_aes' + window_size: 105 + security_policy: 'should_secure' + sak_expiry_time: 99 + confidentiality_offset: 50 + key_server_priority: 11 + interface_type: access + macsec_keys: + - key_name: abc12 + psk: 'AA111111111111111111111111111111111111111111111111111111111111aa' + start_time: '2029-12-11 11:12:13' + end_time: '2030-12-11 11:12:13' + - key_name: ABC + psk: 'AAabcdabcdabcdabcdabcdabcdabcdab11111111111111111111111111111aaa' + state: present + register: nm_add_macsec_policy_2 + + - name: Update the MACsec policy of interface_type 'access' (check mode) + cisco.mso.ndo_macsec_policy: &update_macsec_policy + <<: *add_macsec_policy_2 + description: 'Updated description' + admin_state: 'disabled' + cipher_suite: '128_gcm_aes' + window_size: 110 + security_policy: 'must_secure' + sak_expiry_time: 100 + confidentiality_offset: 30 + key_server_priority: 10 + macsec_keys: + - key_name: abc12 + psk: 'AAabcdabcdabcdabcdabcdabcdabcdab' + start_time: '2029-12-11 11:12:13' + - key_name: ABC + psk: 'AAabcdabcdabcdabcdabcdabcdabcdab' + - key_name: aaa11 + psk: 'AAabcdabcdabcdabcdabcdabcdabcdab' + start_time: '2025-10-10 10:12:13' + end_time: '2026-10-10 10:12:13' + state: present + check_mode: true + register: cm_update_macsec_policy + + - name: Update the MACsec policy of interface_type 'access' + cisco.mso.ndo_macsec_policy: + <<: *update_macsec_policy + register: nm_update_macsec_policy + + # Idempotence for update cannot be checked as the psk is encrypted and the encrypted value changes every time + - name: Update MACsec policy of interface_type 'access' again + cisco.mso.ndo_macsec_policy: + <<: *update_macsec_policy + register: nm_update_macsec_policy_again + + - name: Assert that the MACsec policy was updated + assert: + that: + - cm_update_macsec_policy is changed + - cm_update_macsec_policy.previous.description == 'Ansible MACsec Policy description' + - cm_update_macsec_policy.previous.adminState == 'enabled' + - cm_update_macsec_policy.previous.macsecParams.cipherSuite == '256GcmAes' + - cm_update_macsec_policy.previous.macsecParams.windowSize == 105 + - cm_update_macsec_policy.previous.macsecParams.securityPol == 'shouldSecure' + - cm_update_macsec_policy.previous.macsecParams.sakExpiryTime == 99 + - cm_update_macsec_policy.previous.macsecParams.confOffSet == 'offset50' + - cm_update_macsec_policy.previous.macsecParams.keyServerPrio == 11 + - cm_update_macsec_policy.current.description == cm_update_macsec_policy.proposed.description == 'Updated description' + - cm_update_macsec_policy.current.adminState == cm_update_macsec_policy.proposed.adminState == 'disabled' + - cm_update_macsec_policy.current.macsecParams.cipherSuite == cm_update_macsec_policy.proposed.macsecParams.cipherSuite == '128GcmAes' + - cm_update_macsec_policy.current.macsecParams.windowSize == cm_update_macsec_policy.proposed.macsecParams.windowSize == 110 + - cm_update_macsec_policy.current.macsecParams.securityPol == cm_update_macsec_policy.proposed.macsecParams.securityPol == 'mustSecure' + - cm_update_macsec_policy.current.macsecParams.sakExpiryTime == cm_update_macsec_policy.proposed.macsecParams.sakExpiryTime == 100 + - cm_update_macsec_policy.current.macsecParams.confOffSet == cm_update_macsec_policy.proposed.macsecParams.confOffSet == 'offset30' + - cm_update_macsec_policy.current.macsecParams.keyServerPrio == cm_update_macsec_policy.proposed.macsecParams.keyServerPrio == 10 + - nm_update_macsec_policy is changed + - nm_update_macsec_policy.previous.description == 'Ansible MACsec Policy description' + - nm_update_macsec_policy.previous.adminState == 'enabled' + - nm_update_macsec_policy.previous.macsecParams.cipherSuite == '256GcmAes' + - nm_update_macsec_policy.previous.macsecParams.windowSize == 105 + - nm_update_macsec_policy.previous.macsecParams.securityPol == 'shouldSecure' + - nm_update_macsec_policy.previous.macsecParams.sakExpiryTime == 99 + - nm_update_macsec_policy.previous.macsecParams.confOffSet == 'offset50' + - nm_update_macsec_policy.previous.macsecParams.keyServerPrio == 11 + - nm_update_macsec_policy.previous.macsecKeys | length == 2 + - nm_update_macsec_policy.current.description =='Updated description' + - nm_update_macsec_policy.current.adminState == 'disabled' + - nm_update_macsec_policy.current.macsecParams.cipherSuite == '128GcmAes' + - nm_update_macsec_policy.current.macsecParams.windowSize == 110 + - nm_update_macsec_policy.current.macsecParams.securityPol == 'mustSecure' + - nm_update_macsec_policy.current.macsecParams.sakExpiryTime == 100 + - nm_update_macsec_policy.current.macsecParams.confOffSet == 'offset30' + - nm_update_macsec_policy.current.macsecParams.keyServerPrio == 10 + - nm_update_macsec_policy.current.macsecKeys | length == 3 + - nm_update_macsec_policy.current.uuid is defined + - nm_update_macsec_policy_again is changed + - nm_update_macsec_policy_again.previous.name == cm_update_macsec_policy.current.name == nm_update_macsec_policy.current.name == 'ansible_macsec_policy_2' + - nm_update_macsec_policy_again.previous.type == cm_update_macsec_policy.current.type == nm_update_macsec_policy.current.type == 'access' + - nm_update_macsec_policy_again.previous.description == cm_update_macsec_policy.current.description == nm_update_macsec_policy.current.description == 'Updated description' + - nm_update_macsec_policy_again.current.uuid is defined + - nm_update_macsec_policy_again.previous.uuid is defined + + - name: Update the MACsec policy name + cisco.mso.ndo_macsec_policy: + <<: *mso_info + template: ansible_fabric_policy_template + macsec_policy_uuid: '{{ nm_add_macsec_policy_again.current.uuid }}' + macsec_policy: ansible_macsec_policy_changed + state: present + register: nm_update_macsec_policy_uuid + + - name: Assert that the MACsec policy name was updated + assert: + that: + - nm_update_macsec_policy_uuid is changed + - nm_update_macsec_policy_uuid.previous.name == 'ansible_macsec_policy' + - nm_update_macsec_policy_uuid.current.name == 'ansible_macsec_policy_changed' + - nm_update_macsec_policy_uuid.current.type == nm_update_macsec_policy_uuid.current.type == 'fabric' + - nm_update_macsec_policy_uuid.current.description == nm_update_macsec_policy_uuid.current.description == '' + - nm_update_macsec_policy_uuid.current.adminState == nm_update_macsec_policy_uuid.current.adminState == 'enabled' + - nm_update_macsec_policy_uuid.current.macsecParams.cipherSuite == nm_update_macsec_policy_uuid.current.macsecParams.cipherSuite == '256GcmAesXpn' + - nm_update_macsec_policy_uuid.current.macsecParams.sakExpiryTime == nm_update_macsec_policy_uuid.current.macsecParams.sakExpiryTime == 0 + - nm_update_macsec_policy_uuid.current.macsecParams.securityPol == nm_update_macsec_policy_uuid.current.macsecParams.securityPol == 'shouldSecure' + - nm_update_macsec_policy_uuid.current.macsecParams.windowSize == nm_update_macsec_policy_uuid.current.macsecParams.windowSize == 0 + - nm_update_macsec_policy_uuid.previous.uuid == nm_update_macsec_policy_uuid.current.uuid + - nm_update_macsec_policy_uuid.current.uuid is defined + + - name: Update the MACsec policy by removing the macsec_keys + cisco.mso.ndo_macsec_policy: + <<: *update_macsec_policy + macsec_keys: [] + state: present + register: rm_update_macsec_policy_key + + - name: Assert that the MACsec policy was updated by removing the macsec_keys + assert: + that: + - rm_update_macsec_policy_key is changed + - rm_update_macsec_policy_key.previous.macsecKeys | length == 3 + - rm_update_macsec_policy_key.current.macsecKeys is not defined + + # QUERY + - name: Query a MACsec policy with name + cisco.mso.ndo_macsec_policy: + <<: *mso_info + template: ansible_fabric_policy_template + macsec_policy: ansible_macsec_policy_changed + state: query + register: query_one + + - name: Query all MACsec policies in a template + cisco.mso.ndo_macsec_policy: + <<: *mso_info + template: ansible_fabric_policy_template + state: query + register: query_all + + - name: Assert that the MACsec policy was queried + assert: + that: + - query_one is not changed + - query_one.current.name == 'ansible_macsec_policy_changed' + - query_one.current.type == 'fabric' + - query_one.current.description == '' + - query_all is not changed + - query_all.current | length >= 2 + + - name: Query a MACsec policy with UUID + cisco.mso.ndo_macsec_policy: + <<: *mso_info + template: ansible_fabric_policy_template + macsec_policy_uuid: '{{ nm_update_macsec_policy_uuid.current.uuid }}' + state: query + register: query_uuid + + - name: Assert that the MACsec policy was queried with macsec_policy UUID + assert: + that: + - query_uuid is not changed + - query_uuid.current.name == 'ansible_macsec_policy_changed' + - query_uuid.current.type == 'fabric' + - query_uuid.current.description == '' + + # ERROR + - name: Update the interface_type of exisiting MACsec policy + cisco.mso.ndo_macsec_policy: + <<: *mso_info + template: ansible_fabric_policy_template + macsec_policy: ansible_macsec_policy_changed + interface_type: access + state: present + ignore_errors: true + register: nm_update_macsec_policy_type + + - name: Assert that the MACsec policy interface_type cannot be updated + assert: + that: + - nm_update_macsec_policy_type is failed + - nm_update_macsec_policy_type.msg == 'Type cannot be changed for an existing MACsec Policy.' + + - name: Validate MACsec policy with invalide time format + cisco.mso.ndo_macsec_policy: + <<: *mso_info + template: ansible_fabric_policy_template + macsec_policy: ansible_macsec_policy_2 + description: 'Ansible MACsec Policy description' + interface_type: access + macsec_keys: + - key_name: abc12 + psk: 'AA111111111111111111111111111111111111111111111111111111111111aa' + start_time: 'wrong-time 11:12:13' + end_time: '2030-12-11 11:12:13' + state: present + ignore_errors: true + register: validate_invalid_time + + - name: Assert that the MACsec policy interface_type cannot be updated + assert: + that: + - validate_invalid_time is failed + - validate_invalid_time.msg == "ERROR{{":"}} The time must be in 'YYYY-MM-DD HH:MM:SS' format." + + # DELETE + - name: Delete a MACsec policy with name (check mode) + cisco.mso.ndo_macsec_policy: &delete_macsec_policy + <<: *mso_info + template: ansible_fabric_policy_template + macsec_policy: ansible_macsec_policy_2 + state: absent + check_mode: true + register: cm_delete_macsec_policy + + - name: Delete a MACsec policy with name + cisco.mso.ndo_macsec_policy: + <<: *delete_macsec_policy + register: nm_delete_macsec_policy + + - name: Delete a MACsec policy with name again + cisco.mso.ndo_macsec_policy: + <<: *delete_macsec_policy + register: nm_delete_macsec_policy_again + + - name: Assert that the MACsec policy was deleted + assert: + that: + - cm_delete_macsec_policy is changed + - nm_delete_macsec_policy is changed + - nm_delete_macsec_policy_again is not changed + - nm_delete_macsec_policy.previous.name == 'ansible_macsec_policy_2' + - nm_delete_macsec_policy.current == {} + - nm_delete_macsec_policy_again.current == nm_delete_macsec_policy_again.previous == {} + + - name: Delete a MACsec policy with UUID + cisco.mso.ndo_macsec_policy: + <<: *mso_info + template: ansible_fabric_policy_template + macsec_policy_uuid: '{{ nm_update_macsec_policy_uuid.current.uuid }}' + state: absent + register: delete_macsec_policy_uuid + + - name: Assert that the MACsec policy was deleted using UUID + assert: + that: + - delete_macsec_policy_uuid is changed + - delete_macsec_policy_uuid.previous.name == 'ansible_macsec_policy_changed' + - delete_macsec_policy_uuid.current == {} + + # CLEANUP + - name: Remove fabric template + cisco.mso.ndo_template: + <<: *template_absent \ No newline at end of file