diff --git a/plugins/module_utils/schema.py b/plugins/module_utils/schema.py index 5cb9cedb..5c1e7453 100644 --- a/plugins/module_utils/schema.py +++ b/plugins/module_utils/schema.py @@ -190,6 +190,21 @@ def set_template_anp_epg_useg_attr(self, useg_attr, fail_module=True): self.mso.fail_json(msg=msg) self.schema_objects["template_anp_epg_useg_attribute"] = match + def set_template_anp_epg_annotation(self, annotation_key, fail_module=True): + """ + Get template endpoint group annotation that matches the key of an EPG annotation. + :param useg_attr: Key of the EPG Annotation to match. -> Str + :param fail_module: When match is not found fail the ansible module. -> Bool + :return: Template EPG Annotation item. -> Item(Int, Dict) | None + """ + self.validate_schema_objects_present(["template_anp_epg"]) + kv_list = [KVPair("tagKey", annotation_key)] + match, existing = self.get_object_from_list(self.schema_objects["template_anp_epg"].details.get("tagAnnotations"), kv_list) + if not match and fail_module: + msg = "Provided Annotation Key '{0}' does not match the existing Annotation(s): {1}".format(annotation_key, ", ".join(existing)) + self.mso.fail_json(msg=msg) + self.schema_objects["template_anp_epg_annotation"] = match + def set_template_external_epg(self, external_epg, fail_module=True): """ Get template external epg item that matches the name of an anp. diff --git a/plugins/modules/mso_schema_template_anp_epg_annotation.py b/plugins/modules/mso_schema_template_anp_epg_annotation.py new file mode 100644 index 00000000..859fadea --- /dev/null +++ b/plugins/modules/mso_schema_template_anp_epg_annotation.py @@ -0,0 +1,218 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2024, Samita Bhattacharjee (@samiib) + +# 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: mso_schema_template_anp_epg_annotation +short_description: Manage EPG Annotations on Cisco Nexus Dashboard Orchestrator (NDO). +description: +- Manage Endpoint Group (EPG) Annotations on Cisco Nexus Dashboard Orchestrator (NDO). +- This module is only supported on ND v3.0 (NDO v4.2) and later. +author: +- Samita Bhattacharjee (@samiib) +options: + schema: + description: + - The name of the schema. + type: str + required: true + template: + description: + - The name of the template. + type: str + required: true + anp: + description: + - The name of the Application Network Profile (ANP). + type: str + required: true + epg: + description: + - The name of the EPG to manage. + type: str + required: true + annotation_key: + description: + - The key of the Annotation object. + type: str + aliases: [ key ] + annotation_value: + description: + - The value of the Annotation object. + type: str + aliases: [ value ] + state: + description: + - Use C(present) or C(absent) for adding or removing. + - Use C(query) for listing an object or multiple objects. + type: str + choices: [ absent, present, query ] + default: present +extends_documentation_fragment: cisco.mso.modules +notes: +- The O(schema), O(template), O(anp) and O(epg) must exist before using this module in your playbook. + Use M(cisco.mso.mso_schema_template) to create the schema and template. + Use M(cisco.mso.mso_schema_template_anp) to create the ANP. + Use M(cisco.mso.mso_schema_template_anp_epg) to create the EPG. +seealso: +- module: cisco.mso.mso_schema_template +- module: cisco.mso.mso_schema_template_anp +- module: cisco.mso.mso_schema_template_anp_epg +""" + +EXAMPLES = r""" +- name: Add an annotation with key and value + cisco.mso.mso_schema_template_anp_epg_annotation: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + annotation_key: annotation_key_1 + annotation_value: annotation_value_1 + state: present + +- name: Update an annotation value with key + cisco.mso.mso_schema_template_anp_epg_annotation: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + annotation_key: annotation_key_1 + annotation_value: annotation_value_1_updated + state: present + +- name: Query a specific annotation with key + cisco.mso.mso_schema_template_anp_epg_annotation: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + annotation_key: annotation_key_1 + state: query + register: query_one + +- name: Query all annotations + cisco.mso.mso_schema_template_anp_epg_annotation: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + state: query + register: query_all + +- name: Delete an annotation + cisco.mso.mso_schema_template_anp_epg_annotation: + host: mso_host + username: admin + password: SomeSecretPassword + schema: Schema 1 + template: Template 1 + anp: ANP 1 + epg: EPG 1 + annotation_key: annotation_key_1 + state: absent +""" + +RETURN = r""" +""" + +import copy +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.mso.plugins.module_utils.mso import MSOModule, mso_argument_spec +from ansible_collections.cisco.mso.plugins.module_utils.schema import MSOSchema + + +def main(): + argument_spec = mso_argument_spec() + argument_spec.update( + schema=dict(type="str", required=True), + template=dict(type="str", required=True), + anp=dict(type="str", required=True), + epg=dict(type="str", required=True), + annotation_key=dict(type="str", aliases=["key"], no_log=False), + annotation_value=dict(type="str", aliases=["value"]), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "absent", ["annotation_key"]], + ["state", "present", ["annotation_key", "annotation_value"]], + ], + ) + + schema = module.params.get("schema") + template = module.params.get("template") + anp = module.params.get("anp") + epg = module.params.get("epg") + annotation_key = module.params.get("annotation_key") + annotation_value = module.params.get("annotation_value") + state = module.params.get("state") + + mso = MSOModule(module) + + mso_schema = MSOSchema(mso, schema, template) + mso_schema.set_template(template) + mso_schema.set_template_anp(anp) + mso_schema.set_template_anp_epg(epg) + + if annotation_key: + mso_schema.set_template_anp_epg_annotation(annotation_key, False) + annotation = mso_schema.schema_objects.get("template_anp_epg_annotation") + if annotation is not None: + mso.existing = mso.previous = copy.deepcopy(annotation.details) # Query a specific Annotation + else: + epg_object = mso_schema.schema_objects["template_anp_epg"] + mso.existing = epg_object.details.get("tagAnnotations", []) # Query all + + path = "/templates/{0}/anps/{1}/epgs/{2}/tagAnnotations".format(template, anp, epg) + + ops = [] + if state == "present": + mso_values = {"tagKey": annotation_key, "tagValue": annotation_value} + mso.sanitize(mso_values) + if mso.existing: + if annotation_value is not None and mso.existing.get("tagValue") != annotation_value: + ops.append({"op": "replace", "path": "{0}/{1}".format(path, annotation.index), "value": mso_values}) + + else: + ops.append({"op": "add", "path": "{0}/-".format(path), "value": mso_values}) + elif state == "absent": + if mso.existing: + ops.append({"op": "remove", "path": "{0}/{1}".format(path, annotation.index)}) + + if not module.check_mode and ops: + mso.request(mso_schema.path, method="PATCH", data=ops) + mso.existing = mso.proposed + elif module.check_mode and state != "query": # When the state is present/absent with check mode + mso.existing = mso.proposed if state == "present" else {} + + mso.exit_json() + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/mso_schema_template_anp_epg_annotation/aliases b/tests/integration/targets/mso_schema_template_anp_epg_annotation/aliases new file mode 100644 index 00000000..5042c9c0 --- /dev/null +++ b/tests/integration/targets/mso_schema_template_anp_epg_annotation/aliases @@ -0,0 +1,2 @@ +# No ACI MultiSite infrastructure, so not enabled +# unsupported diff --git a/tests/integration/targets/mso_schema_template_anp_epg_annotation/tasks/main.yml b/tests/integration/targets/mso_schema_template_anp_epg_annotation/tasks/main.yml new file mode 100644 index 00000000..fe2f7232 --- /dev/null +++ b/tests/integration/targets/mso_schema_template_anp_epg_annotation/tasks/main.yml @@ -0,0 +1,249 @@ +# Test code for the MSO modules + +# Copyright: (c) 2024, Samita Bhattacharjee (@samiib) + +# 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 NDO version >= 4.2 + when: version.current.version is version('4.2', '>=') + block: + + # SETUP + - name: Remove schemas + cisco.mso.mso_schema: &ansible_schema_absent + <<: *mso_info + schema: ansible_test + state: absent + + - name: Ensure ansible_test tenant absent + cisco.mso.mso_tenant: &ansible_tenant_absent + <<: *mso_info + tenant: ansible_test + users: + - "{{ mso_username }}" + state: absent + + - name: Ensure ansible_test tenant present + cisco.mso.mso_tenant: &ansible_tenant_present + <<: *ansible_tenant_absent + state: present + + - name: Ensure ansible_test schema with ans_test_template exist + cisco.mso.mso_schema_template: + <<: *mso_info + schema: ansible_test + tenant: ansible_test + template: ans_test_template + state: present + + - name: Ensure ans_test_anp exist + cisco.mso.mso_schema_template_anp: + <<: *mso_info + schema: ansible_test + template: ans_test_template + anp: ans_test_anp + state: present + + - name: Ensure ans_test_epg exist + cisco.mso.mso_schema_template_anp_epg: &ans_test_epg_present + <<: *mso_info + schema: ansible_test + template: ans_test_template + anp: ans_test_anp + epg: ans_test_epg + state: present + + # CREATE + - name: Create an EPG annotation (check mode) + cisco.mso.mso_schema_template_anp_epg_annotation: &add_annotation_1 + <<: *ans_test_epg_present + annotation_key: annotation_key_1 + annotation_value: annotation_value_1 + check_mode: true + register: cm_add_epg_annotation + + - name: Create an EPG annotation + cisco.mso.mso_schema_template_anp_epg_annotation: + <<: *add_annotation_1 + register: nm_add_epg_annotation + + - name: Create an EPG annotation again + cisco.mso.mso_schema_template_anp_epg_annotation: + <<: *add_annotation_1 + register: nm_add_epg_annotation_again + + - name: Assert that the EPG annotation was created + ansible.builtin.assert: + that: + - cm_add_epg_annotation is changed + - cm_add_epg_annotation.previous == nm_add_epg_annotation.previous == {} + - cm_add_epg_annotation.current.tagKey == cm_add_epg_annotation.proposed.tagKey == 'annotation_key_1' + - cm_add_epg_annotation.current.tagValue == cm_add_epg_annotation.proposed.tagValue == 'annotation_value_1' + - nm_add_epg_annotation is changed + - nm_add_epg_annotation.current.tagKey == 'annotation_key_1' + - nm_add_epg_annotation.current.tagValue == 'annotation_value_1' + - nm_add_epg_annotation_again is not changed + - nm_add_epg_annotation_again.sent == {} + - nm_add_epg_annotation_again.previous.tagKey == nm_add_epg_annotation_again.current.tagKey == 'annotation_key_1' + - nm_add_epg_annotation_again.previous.tagValue == nm_add_epg_annotation_again.current.tagValue == 'annotation_value_1' + + # UPDATE + - name: Update an EPG annotation (check mode) + cisco.mso.mso_schema_template_anp_epg_annotation: &update_annotation_1 + <<: *add_annotation_1 + annotation_value: "annotation_value_1_updated" + check_mode: true + register: cm_update_epg_annotation + + - name: Update an EPG annotation + cisco.mso.mso_schema_template_anp_epg_annotation: + <<: *update_annotation_1 + register: nm_update_epg_annotation + + - name: Update an EPG annotation again + cisco.mso.mso_schema_template_anp_epg_annotation: + <<: *update_annotation_1 + register: nm_update_epg_annotation_again + + - name: Assert that the EPG annotation was updated + ansible.builtin.assert: + that: + - cm_update_epg_annotation is changed + - cm_update_epg_annotation.previous == nm_update_epg_annotation.previous == nm_add_epg_annotation.current + - cm_update_epg_annotation.current.tagKey == cm_update_epg_annotation.proposed.tagKey == 'annotation_key_1' + - cm_update_epg_annotation.current.tagValue == cm_update_epg_annotation.proposed.tagValue == 'annotation_value_1_updated' + - nm_update_epg_annotation is changed + - nm_update_epg_annotation.current.tagKey == 'annotation_key_1' + - nm_update_epg_annotation.current.tagValue == 'annotation_value_1_updated' + - nm_update_epg_annotation_again is not changed + - nm_update_epg_annotation_again.sent == {} + - nm_update_epg_annotation_again.previous.tagKey == nm_update_epg_annotation_again.current.tagKey == 'annotation_key_1' + - nm_update_epg_annotation_again.previous.tagValue == nm_update_epg_annotation_again.current.tagValue == 'annotation_value_1_updated' + + # QUERY + - name: Create another EPG annotation + cisco.mso.mso_schema_template_anp_epg_annotation: + <<: *add_annotation_1 + annotation_key: annotation_key_2 + annotation_value: annotation_value_2 + + - name: Query all EPG annotations + cisco.mso.mso_schema_template_anp_epg_annotation: &query_all + <<: *ans_test_epg_present + state: query + register: query_all + + - name: Query one EPG annotation + cisco.mso.mso_schema_template_anp_epg_annotation: &query_one + <<: *ans_test_epg_present + annotation_key: annotation_key_2 + state: query + ignore_errors: true + register: query_one + + - name: Verify query_one and query_all + ansible.builtin.assert: + that: + - query_all is not changed + - query_one is not changed + - query_all.current | length == 2 + - query_all.current.0.tagKey == "annotation_key_1" + - query_all.current.1.tagKey == "annotation_key_2" + - query_one.current.tagKey == "annotation_key_2" + - query_one.current.tagValue == "annotation_value_2" + + # DELETE + - name: Delete an EPG annotation (check mode) + cisco.mso.mso_schema_template_anp_epg_annotation: &delete_annotation_1 + <<: *add_annotation_1 + state: absent + check_mode: true + register: cm_delete_epg_annotation + + - name: Delete an EPG annotation + cisco.mso.mso_schema_template_anp_epg_annotation: + <<: *delete_annotation_1 + register: nm_delete_epg_annotation + + - name: Delete an EPG annotation again + cisco.mso.mso_schema_template_anp_epg_annotation: + <<: *delete_annotation_1 + register: nm_delete_epg_annotation_again + + - name: Assert that the Interface Policy group with name was deleted + ansible.builtin.assert: + that: + - cm_delete_epg_annotation is changed + - nm_delete_epg_annotation is changed + - cm_delete_epg_annotation.current == nm_delete_epg_annotation.current == {} + - cm_delete_epg_annotation.previous.tagKey == nm_delete_epg_annotation.previous.tagKey == "annotation_key_1" + - cm_delete_epg_annotation.previous.tagValue == nm_delete_epg_annotation.previous.tagValue == "annotation_value_1_updated" + - nm_delete_epg_annotation_again is not changed + - nm_delete_epg_annotation_again.previous == nm_delete_epg_annotation_again.current == {} + + # ERRORS + - name: Delete the remaining EPG annotation + cisco.mso.mso_schema_template_anp_epg_annotation: + <<: *delete_annotation_1 + annotation_key: annotation_key_2 + + - name: Query all EPG annotations when none exist + cisco.mso.mso_schema_template_anp_epg_annotation: + <<: *query_all + register: query_all_none + + - name: Query an EPG annotation that does not exist + cisco.mso.mso_schema_template_anp_epg_annotation: + <<: *query_one + annotation_key: annotation_key_3 + register: query_one_none + + - name: Create an EPG annotation when EPG does not exist + cisco.mso.mso_schema_template_anp_epg_annotation: + <<: *add_annotation_1 + epg: ans_test_epg_none + ignore_errors: true + register: non_existing_epg + + - name: Assert error conditions + ansible.builtin.assert: + that: + - query_all_none is not changed + - query_all_none.current == [] + - query_one_none is not changed + - query_one_none.current == {} + - non_existing_epg is failed + - non_existing_epg.msg == "Provided EPG 'ans_test_epg_none' not matching existing epg(s){{":"}} ans_test_epg" + + # CLEANUP + - name: Remove ansible_test schema + cisco.mso.mso_schema: + <<: *ansible_schema_absent + + - name: Remove ansible_test tenant + cisco.mso.mso_tenant: + <<: *ansible_tenant_absent