diff --git a/plugins/module_utils/constants.py b/plugins/module_utils/constants.py index a0bba1713..ee94fc5e3 100644 --- a/plugins/module_utils/constants.py +++ b/plugins/module_utils/constants.py @@ -173,6 +173,11 @@ provider="vzProvSubjLbl", ) +SUBJ_LABEL_RN = dict( + consumer="conssubjlbl-", + provider="provsubjlbl-", +) + MATCH_ACTION_RULE_SET_METRIC_TYPE_MAPPING = {"ospf_type_1": "ospf-type1", "ospf_type_2": "ospf-type2", "": ""} MATCH_EIGRP_INTERFACE_POLICY_DELAY_UNIT_MAPPING = dict(picoseconds="pico", tens_of_microseconds="tens-of-micro") @@ -284,3 +289,146 @@ OPERATOR_MAPPING = dict(equals="equals", contains="contains", starts_with="startsWith", ends_with="endsWith") MATCH_STORM_CONTROL_POLICY_TYPE_MAPPING = dict(all_types="Invalid", unicast_broadcast_multicast="Valid") + +POLICY_LABEL_COLORS = [ + "alice_blue", + "antique_white", + "aqua", + "aquamarine", + "azure", + "beige", + "bisque", + "black", + "blanched_almond", + "blue", + "blue_violet", + "brown", + "burlywood", + "cadet_blue", + "chartreuse", + "chocolate", + "coral", + "cornflower_blue", + "cornsilk", + "crimson", + "cyan", + "dark_blue", + "dark_cyan", + "dark_goldenrod", + "dark_gray", + "dark_green", + "dark_khaki", + "dark_magenta", + "dark_olive_green", + "dark_orange", + "dark_orchid", + "dark_red", + "dark_salmon", + "dark_sea_green", + "dark_slate_blue", + "dark_slate_gray", + "dark_turquoise", + "dark_violet", + "deep_pink", + "deep_sky_blue", + "dim_gray", + "dodger_blue", + "fire_brick", + "floral_white", + "forest_green", + "fuchsia", + "gainsboro", + "ghost_white", + "gold", + "goldenrod", + "gray", + "green", + "green_yellow", + "honeydew", + "hot_pink", + "indian_red", + "indigo", + "ivory", + "khaki", + "lavender", + "lavender_blush", + "lawn_green", + "lemon_chiffon", + "light_blue", + "light_coral", + "light_cyan", + "light_goldenrod_yellow", + "light_gray", + "light_green", + "light_pink", + "light_salmon", + "light_sea_green", + "light_sky_blue", + "light_slate_gray", + "light_steel_blue", + "light_yellow", + "lime", + "lime_green", + "linen", + "magenta", + "maroon", + "medium_aquamarine", + "medium_blue", + "medium_orchid", + "medium_purple", + "medium_sea_green", + "medium_slate_blue", + "medium_spring_green", + "medium_turquoise", + "medium_violet_red", + "midnight_blue", + "mint_cream", + "misty_rose", + "moccasin", + "navajo_white", + "navy", + "old_lace", + "olive", + "olive_drab", + "orange", + "orange_red", + "orchid", + "pale_goldenrod", + "pale_green", + "pale_turquoise", + "pale_violet_red", + "papaya_whip", + "peachpuff", + "peru", + "pink", + "plum", + "powder_blue", + "purple", + "red", + "rosy_brown", + "royal_blue", + "saddle_brown", + "salmon", + "sandy_brown", + "sea_green", + "seashell", + "sienna", + "silver", + "sky_blue", + "slate_blue", + "slate_gray", + "snow", + "spring_green", + "steel_blue", + "tan", + "teal", + "thistle", + "tomato", + "turquoise", + "violet", + "wheat", + "white", + "white_smoke", + "yellow", + "yellow_green", +] diff --git a/plugins/modules/aci_subject_label.py b/plugins/modules/aci_subject_label.py new file mode 100644 index 000000000..a68c39d8f --- /dev/null +++ b/plugins/modules/aci_subject_label.py @@ -0,0 +1,746 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, Mark Ciecior (@markciecior) +# Copyright: (c) 2024, Akini Ross +# 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": "certified"} + +DOCUMENTATION = r""" +--- +module: aci_subject_label +short_description: Manage Subject Labels (vz:ConsSubjLbl and vz:ProvSubjLbl) +description: +- Manage Subject Labels on Cisco ACI fabrics. +options: + tenant: + description: + - The name of the Tenant. + type: str + aliases: [ tenant_name ] + l2out: + description: + - The name of the L2Out. + type: str + aliases: [ l2out_name ] + l3out: + description: + - The name of the L3Out. + type: str + aliases: [ l3out_name ] + external_epg: + description: + - The name of the External End Point Group. + type: str + aliases: [ extepg, extepg_name, external_epg_name ] + contract: + description: + - The name of the Contract. + type: str + aliases: [ contract_name ] + subject: + description: + - The name of the Subject. + type: str + aliases: [ subject_name ] + ap: + description: + - The name of the Application Profile. + type: str + aliases: [ app_profile, app_profile_name, application_profile, application_profile_name] + epg: + description: + - The name of the End Point Group. + type: str + aliases: [ epg_name ] + esg: + description: + - The name of the Endpoint Security Group. + type: str + aliases: [ esg_name ] + subject_label: + description: + - The name of the Subject Label. + type: str + aliases: [ subject_label_name, name, label ] + subject_label_type: + description: + - Determines the type of the Subject Label. + type: str + required: true + choices: [ consumer, provider ] + aliases: [ type ] + complement: + description: + - Whether complement is enabled on the Subject Label. + - The APIC defaults to C(false) when unset during creation. + type: bool + tag: + description: + - The color of a policy label of the Subject Label. + - The APIC defaults to C(yellow-green) when unset during creation. + type: str + choices: + - alice_blue + - antique_white + - aqua + - aquamarine + - azure + - beige + - bisque + - black + - blanched_almond + - blue + - blue_violet + - brown + - burlywood + - cadet_blue + - chartreuse + - chocolate + - coral + - cornflower_blue + - cornsilk + - crimson + - cyan + - dark_blue + - dark_cyan + - dark_goldenrod + - dark_gray + - dark_green + - dark_khaki + - dark_magenta + - dark_olive_green + - dark_orange + - dark_orchid + - dark_red + - dark_salmon + - dark_sea_green + - dark_slate_blue + - dark_slate_gray + - dark_turquoise + - dark_violet + - deep_pink + - deep_sky_blue + - dim_gray + - dodger_blue + - fire_brick + - floral_white + - forest_green + - fuchsia + - gainsboro + - ghost_white + - gold + - goldenrod + - gray + - green + - green_yellow + - honeydew + - hot_pink + - indian_red + - indigo + - ivory + - khaki + - lavender + - lavender_blush + - lawn_green + - lemon_chiffon + - light_blue + - light_coral + - light_cyan + - light_goldenrod_yellow + - light_gray + - light_green + - light_pink + - light_salmon + - light_sea_green + - light_sky_blue + - light_slate_gray + - light_steel_blue + - light_yellow + - lime + - lime_green + - linen + - magenta + - maroon + - medium_aquamarine + - medium_blue + - medium_orchid + - medium_purple + - medium_sea_green + - medium_slate_blue + - medium_spring_green + - medium_turquoise + - medium_violet_red + - midnight_blue + - mint_cream + - misty_rose + - moccasin + - navajo_white + - navy + - old_lace + - olive + - olive_drab + - orange + - orange_red + - orchid + - pale_goldenrod + - pale_green + - pale_turquoise + - pale_violet_red + - papaya_whip + - peachpuff + - peru + - pink + - plum + - powder_blue + - purple + - red + - rosy_brown + - royal_blue + - saddle_brown + - salmon + - sandy_brown + - sea_green + - seashell + - sienna + - silver + - sky_blue + - slate_blue + - slate_gray + - snow + - spring_green + - steel_blue + - tan + - teal + - thistle + - tomato + - turquoise + - violet + - wheat + - white + - white_smoke + - yellow + - yellow_green + description: + description: + - The description for the Subject Label. + type: str + aliases: [ descr ] + 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 + name_alias: + description: + - The alias for the current object. This relates to the nameAlias field in ACI. + type: str +extends_documentation_fragment: +- cisco.aci.aci +- cisco.aci.annotation +- cisco.aci.owner + +seealso: +- name: APIC Management Information Model reference + description: More information about the internal APIC class B(vz:ConsSubjLbl) and (vz:ProvSubjLbl). + link: https://developer.cisco.com/docs/apic-mim-ref/ +author: +- Mark Ciecior (@markciecior) +- Akini Ross (@akinross) +""" + +EXAMPLES = r""" +- name: Add a Subject Label on a Contract Subject + cisco.aci.aci_subject_label: + host: apic + username: admin + password: SomeSecretPassword + tenant: production + contract: web + subject: web_subject + subject_label: web_subject_label + subject_type: consumer + state: present + delegate_to: localhost + +- name: Add a Subject Label on a L2Out External EPG + cisco.aci.aci_subject_label: + host: apic + username: admin + password: SomeSecretPassword + tenant: production + l2out: l2out_name + external_epg: external_epg_name + subject_label: l2out_subject_label + subject_type: consumer + state: present + delegate_to: localhost + +- name: Add a Subject Label on a L3Out External EPG + cisco.aci.aci_subject_label: + host: apic + username: admin + password: SomeSecretPassword + tenant: production + l3out: l3out_name + external_epg: external_epg_name + subject_label: l3out_subject_label + subject_type: consumer + state: present + delegate_to: localhost + +- name: Add a Subject Label on a L3Out External EPG Contract + cisco.aci.aci_subject_label: + host: apic + username: admin + password: SomeSecretPassword + tenant: production + l3out: l3out_name + external_epg: external_epg_name + contract: web + subject_label: l3out_subject_label + subject_type: consumer + state: present + delegate_to: localhost + +- name: Add a Subject Label on a ESG + cisco.aci.aci_subject_label: + host: apic + username: admin + password: SomeSecretPassword + tenant: production + ap: app_profile_name + esg: esg_name + subject_label: esg_subject_label + subject_type: consumer + state: present + delegate_to: localhost + +- name: Add a Subject Label on a EPG + cisco.aci.aci_subject_label: + host: apic + username: admin + password: SomeSecretPassword + tenant: production + ap: app_profile_name + epg: epg_name + subject_label: epg_subject_label + subject_type: consumer + state: present + delegate_to: localhost + +- name: Add a Subject Label on a EPG Contract + cisco.aci.aci_subject_label: + host: apic + username: admin + password: SomeSecretPassword + tenant: production + ap: app_profile_name + epg: epg_name + contract: web + subject_label: epg_subject_label + subject_type: consumer + state: present + delegate_to: localhost + +- name: Query a Subject Label on a Contract Subject + cisco.aci.aci_subject_label: + host: apic + username: admin + password: SomeSecretPassword + tenant: production + contract: web + subject: web_subject + subject_label: web_subject_label + subject_type: consumer + state: query + delegate_to: localhost + register: query_result + +- name: Query all Subject Labels + cisco.aci.aci_subject_label: + host: apic + username: admin + password: SomeSecretPassword + state: query + delegate_to: localhost + register: query_result + +- name: Remove a Subject Label on a Contract Subject + cisco.aci.aci_subject_label: + host: apic + username: admin + password: SomeSecretPassword + tenant: production + contract: web + subject: web_subject + subject_label: web_subject_label + subject_type: consumer + state: absent + delegate_to: localhost +""" + +RETURN = r""" +current: + description: The existing configuration from the APIC after the module has finished + returned: success + type: list + sample: + [ + { + "fvTenant": { + "attributes": { + "descr": "Production environment", + "dn": "uni/tn-production", + "name": "production", + "nameAlias": "", + "ownerKey": "", + "ownerTag": "" + } + } + } + ] +error: + description: The error information as returned from the APIC + returned: failure + type: dict + sample: + { + "code": "122", + "text": "unknown managed object class foo" + } +raw: + description: The raw output returned by the APIC REST API (xml or json) + returned: parse error + type: str + sample: '' +sent: + description: The actual/minimal configuration pushed to the APIC + returned: info + type: list + sample: + { + "fvTenant": { + "attributes": { + "descr": "Production environment" + } + } + } +previous: + description: The original configuration from the APIC before the module has started + returned: info + type: list + sample: + [ + { + "fvTenant": { + "attributes": { + "descr": "Production", + "dn": "uni/tn-production", + "name": "production", + "nameAlias": "", + "ownerKey": "", + "ownerTag": "" + } + } + } + ] +proposed: + description: The assembled configuration from the user-provided parameters + returned: info + type: dict + sample: + { + "fvTenant": { + "attributes": { + "descr": "Production environment", + "name": "production" + } + } + } +filter_string: + description: The filter string used for the request + returned: failure or debug + type: str + sample: ?rsp-prop-include=config-only +method: + description: The HTTP method used for the request to the APIC + returned: failure or debug + type: str + sample: POST +response: + description: The HTTP response from the APIC + returned: failure or debug + type: str + sample: OK (30 bytes) +status: + description: The HTTP status from the APIC + returned: failure or debug + type: int + sample: 200 +url: + description: The HTTP url used for the request to the APIC + returned: failure or debug + type: str + sample: https://10.11.12.13/api/mo/uni/tn-production.json +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.aci.plugins.module_utils.aci import ACIModule, aci_argument_spec, aci_annotation_spec, aci_owner_spec +from ansible_collections.cisco.aci.plugins.module_utils.constants import ACI_CLASS_MAPPING, SUBJ_LABEL_MAPPING, SUBJ_LABEL_RN, POLICY_LABEL_COLORS + + +def main(): + argument_spec = aci_argument_spec() + argument_spec.update(aci_annotation_spec()) + argument_spec.update(aci_owner_spec()) + argument_spec.update( + tenant=dict(type="str", aliases=["tenant_name"]), + l2out=dict(type="str", aliases=["l2out_name"]), + l3out=dict(type="str", aliases=["l3out_name"]), + external_epg=dict(type="str", aliases=["extepg", "extepg_name", "external_epg_name"]), + contract=dict(type="str", aliases=["contract_name"]), + subject=dict(type="str", aliases=["subject_name"]), + ap=dict(type="str", aliases=["app_profile", "app_profile_name", "application_profile", "application_profile_name"]), + epg=dict(type="str", aliases=["epg_name"]), + esg=dict(type="str", aliases=["esg_name"]), + complement=dict(type="bool"), + description=dict(type="str", aliases=["descr"]), + subject_label=dict(type="str", aliases=["subject_label_name", "name", "label"]), + subject_label_type=dict(type="str", choices=["consumer", "provider"], aliases=["type"], required=True), + tag=dict(type="str", choices=POLICY_LABEL_COLORS), + state=dict(type="str", default="present", choices=["absent", "present", "query"]), + name_alias=dict(type="str"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ["state", "present", ["tenant", "subject_label"]], + ["state", "present", ["l2out", "l3out", "epg", "esg", "subject"], True], + ["state", "absent", ["tenant", "subject_label"]], + ["state", "absent", ["l2out", "l3out", "epg", "esg", "subject"], True], + ], + mutually_exclusive=[ + ["l2out", "l3out", "epg", "esg", "subject"], + ["esg", "contract"], + ["l2out", "contract"], + ], + required_by={ + "subject": ["contract"], + "l2out": ["external_epg"], + "l3out": ["external_epg"], + "epg": ["ap"], + "esg": ["ap"], + }, + ) + + aci = ACIModule(module) + + tenant = module.params.get("tenant") + l2out = module.params.get("l2out") + l3out = module.params.get("l3out") + external_epg = module.params.get("external_epg") + contract = module.params.get("contract") + subject_label_type = module.params.get("subject_label_type") + subject = module.params.get("subject") + ap = module.params.get("ap") + epg = module.params.get("epg") + esg = module.params.get("esg") + complement = aci.boolean(module.params.get("complement")) + description = module.params.get("description") + subject_label = module.params.get("subject_label") + tag = module.params.get("tag") + state = module.params.get("state") + name_alias = module.params.get("name_alias") + + aci_class = SUBJ_LABEL_MAPPING.get(subject_label_type) + aci_rn = SUBJ_LABEL_RN.get(subject_label_type) + subject_label if subject_label else None + + if contract: + contract_rn = ACI_CLASS_MAPPING.get(subject_label_type).get("rn") + contract + contract_class = ACI_CLASS_MAPPING.get(subject_label_type).get("class") + + root_class = dict( + aci_class="fvTenant", + aci_rn="tn-{0}".format(tenant), + module_object=tenant, + target_filter={"name": tenant}, + ) + subclass_1 = None + subclass_2 = None + subclass_3 = None + subclass_4 = None + if esg: + subclass_1 = dict( + aci_class="fvAp", + aci_rn="ap-{0}".format(ap), + module_object=ap, + target_filter={"name": ap}, + ) + subclass_2 = dict( + aci_class="fvESg", + aci_rn="esg-{0}".format(esg), + module_object=esg, + target_filter={"name": esg}, + ) + subclass_3 = dict( + aci_class=aci_class, + aci_rn=aci_rn, + module_object=subject_label, + target_filter={"name": subject_label}, + ) + elif l2out: + subclass_1 = dict( + aci_class="l2extOut", + aci_rn="l2out-{0}".format(l2out), + module_object=l2out, + target_filter={"name": l2out}, + ) + subclass_2 = dict( + aci_class="l2extInstP", + aci_rn="instP-{0}".format(external_epg), + module_object=external_epg, + target_filter={"name": external_epg}, + ) + subclass_3 = dict( + aci_class=aci_class, + aci_rn=aci_rn, + module_object=subject_label, + target_filter={"name": subject_label}, + ) + elif epg: + subclass_1 = dict( + aci_class="fvAp", + aci_rn="ap-{0}".format(ap), + module_object=ap, + target_filter={"name": ap}, + ) + subclass_2 = dict( + aci_class="fvAEPg", + aci_rn="epg-{0}".format(epg), + module_object=epg, + target_filter={"name": epg}, + ) + if contract: + subclass_3 = dict( + aci_class=contract_class, + aci_rn=contract_rn, + module_object=contract, + target_filter={"name": contract}, + ) + subclass_4 = dict( + aci_class=aci_class, + aci_rn=aci_rn, + module_object=subject_label, + target_filter={"name": subject_label}, + ) + else: + subclass_3 = dict( + aci_class=aci_class, + aci_rn=aci_rn, + module_object=subject_label, + target_filter={"name": subject_label}, + ) + elif l3out: + subclass_1 = subclass_1 = dict( + aci_class="l3extOut", + aci_rn="out-{0}".format(l3out), + module_object=l3out, + target_filter={"name": l3out}, + ) + subclass_2 = dict( + aci_class="l3extInstP", + aci_rn="instP-{0}".format(external_epg), + module_object=external_epg, + target_filter={"name": external_epg}, + ) + if contract: + subclass_3 = dict( + aci_class=contract_class, + aci_rn=contract_rn, + module_object=contract, + target_filter={"name": contract}, + ) + subclass_4 = dict( + aci_class=aci_class, + aci_rn=aci_rn, + module_object=subject_label, + target_filter={"name": subject_label}, + ) + else: + subclass_3 = dict( + aci_class=aci_class, + aci_rn=aci_rn, + module_object=subject_label, + target_filter={"name": subject_label}, + ) + elif subject: + subclass_1 = dict( + aci_class="vzBrCP", + aci_rn="brc-{0}".format(contract), + module_object=contract, + target_filter={"name": contract}, + ) + subclass_2 = dict( + aci_class="vzSubj", + aci_rn="subj-{0}".format(subject), + module_object=subject, + target_filter={"name": subject}, + ) + subclass_3 = dict( + aci_class=aci_class, + aci_rn=aci_rn, + module_object=subject_label, + target_filter={"name": subject_label}, + ) + else: # Query scenario without any filters forcing class query on the subject_label_class + root_class = dict( + aci_class=aci_class, + aci_rn=aci_rn, + module_object=subject_label, + target_filter={"name": subject_label}, + ) + + aci.construct_url( + root_class=root_class, + subclass_1=subclass_1, + subclass_2=subclass_2, + subclass_3=subclass_3, + subclass_4=subclass_4, + ) + + aci.get_existing() + + if state == "present": + aci.payload( + aci_class=aci_class, + class_config=dict( + name=subject_label, + descr=description, + nameAlias=name_alias, + isComplement=complement, + tag=tag.replace("_", "-") if tag else None, + ), + ) + + aci.get_diff(aci_class=aci_class) + + aci.post_config() + + elif state == "absent": + aci.delete_config() + + aci.exit_json() + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/aci_subject_label/aliases b/tests/integration/targets/aci_subject_label/aliases new file mode 100644 index 000000000..209b793f9 --- /dev/null +++ b/tests/integration/targets/aci_subject_label/aliases @@ -0,0 +1,2 @@ +# No ACI simulator yet, so not enabled +# unsupported diff --git a/tests/integration/targets/aci_subject_label/tasks/main.yml b/tests/integration/targets/aci_subject_label/tasks/main.yml new file mode 100644 index 000000000..eb39aabf5 --- /dev/null +++ b/tests/integration/targets/aci_subject_label/tasks/main.yml @@ -0,0 +1,253 @@ +# Test code for the ACI modules + +# Copyright: (c) 2022, Mark Ciecior (@markciecior) +# Copyright: (c) 2024, Akini Ross (akinross@cisco.com) + +# 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 APIC host, ACI username and ACI password + ansible.builtin.fail: + msg: 'Please define the following variables: aci_hostname, aci_username and aci_password.' + when: aci_hostname is not defined or aci_username is not defined or aci_password is not defined + +- name: Set vars + ansible.builtin.set_fact: + aci_info: &aci_info + host: "{{ aci_hostname }}" + username: "{{ aci_username }}" + password: "{{ aci_password }}" + validate_certs: '{{ aci_validate_certs | default(false) }}' + use_ssl: '{{ aci_use_ssl | default(true) }}' + use_proxy: '{{ aci_use_proxy | default(true) }}' + output_level: '{{ aci_output_level | default("info") }}' + +- name: Query system information + cisco.aci.aci_system: + <<: *aci_info + id: 1 + state: query + register: version + +- name: Verify Cloud and Non-Cloud Sites in use. + ansible.builtin.include_tasks: ../../../../../../integration/targets/aci_cloud_provider/tasks/main.yml + +- name: Execute tasks only for non-cloud sites + when: + - query_cloud.current == [] + block: + + # CLEAN TEST ENVIRONMENT + - name: Ensure tenant removed + cisco.aci.aci_tenant: &aci_tenant_absent + <<: *aci_info + state: absent + tenant: ansible_test + + # SETUP TEST ENVIRONMENT + - name: Create tenant + cisco.aci.aci_tenant: &aci_tenant_present + <<: *aci_tenant_absent + state: present + + - name: Create BD + cisco.aci.aci_bd: + <<: *aci_tenant_present + bd: ansible_test_bd + + - name: Create VRF + cisco.aci.aci_vrf: + <<: *aci_tenant_present + vrf: ansible_test_vrf + + - name: Create AP + cisco.aci.aci_ap: &aci_ap + <<: *aci_tenant_present + ap: ansible_test_ap + + - name: Create EPG + cisco.aci.aci_epg: + <<: *aci_ap + epg: ansible_test_epg + bd: ansible_test_bd + + - name: Bind End Point Group to Provider Contract + cisco.aci.aci_epg_to_contract: + <<: *aci_tenant_present + ap: ansible_test_ap + epg: ansible_test_epg + contract_type: provider + contract: ansible_test_contract + + - name: Bind End Point Group to Consumer Contract + cisco.aci.aci_epg_to_contract: + <<: *aci_tenant_present + ap: ansible_test_ap + epg: ansible_test_epg + contract_type: consumer + contract: ansible_test_contract + + - name: Create ESG + cisco.aci.aci_esg: + <<: *aci_ap + esg: ansible_test_esg + vrf: ansible_test_vrf + when: version.current.0.topSystem.attributes.version is version('5', '>=') + + - name: Create L2Out + cisco.aci.aci_l2out: + <<: *aci_tenant_present + l2out: ansible_test_l2out + bd: ansible_test_bd + domain: l2Dom + vlan: 3200 + + - name: Create L2Out External End Point Group + cisco.aci.aci_l2out_extepg: + <<: *aci_tenant_present + l2out: ansible_test_l2out + extepg: ansible_test_external_epg + + - name: Create L2Out + cisco.aci.aci_l3out: + <<: *aci_tenant_present + l3out: ansible_test_l3out + vrf: ansible_test_vrf + domain: l3dom + + - name: Create L3Out External End Point Group + cisco.aci.aci_l3out_extepg: + <<: *aci_tenant_present + l3out: ansible_test_l3out + extepg: ansible_test_external_epg + + - name: Bind L3out External End Point Group to Provider Contract + cisco.aci.aci_l3out_extepg_to_contract: + <<: *aci_tenant_present + tenant: ansible_test + l3out: ansible_test_l3out + extepg: ansible_test_external_epg + contract: ansible_test_contract + contract_type: provider + + - name: Bind L3out External End Point Group to Consumer Contract + cisco.aci.aci_l3out_extepg_to_contract: + <<: *aci_tenant_present + tenant: ansible_test + l3out: ansible_test_l3out + extepg: ansible_test_external_epg + contract: ansible_test_contract + contract_type: consumer + + - name: Create Contract + cisco.aci.aci_contract: &aci_contract_present + <<: *aci_tenant_present + contract: ansible_test_contract + + - name: Create Subject + cisco.aci.aci_contract_subject: + <<: *aci_contract_present + subject: ansible_test_subject + + # TESTS THAT ALL PARENT CLASS COMBINATIONS ARE CONFIGURABLE + - name: Execute tests for each parent class + ansible.builtin.include_tasks: subject_label.yml + loop: + - { l2out: "{{ fake_var | default(omit) }}", l3out: "{{ fake_var | default(omit) }}", external_epg: "{{ fake_var | default(omit) }}", contract: "ansible_test_contract", subject: "ansible_test_subject", ap: "{{ fake_var | default(omit) }}", epg: "{{ fake_var | default(omit) }}", esg: "{{ fake_var | default(omit) }}"} + - { l2out: "ansible_test_l2out", l3out: "{{ fake_var | default(omit) }}", external_epg: "ansible_test_external_epg", contract: "{{ fake_var | default(omit) }}", subject: "{{ fake_var | default(omit) }}", ap: "{{ fake_var | default(omit) }}", epg: "{{ fake_var | default(omit) }}", esg: "{{ fake_var | default(omit) }}"} + - { l2out: "{{ fake_var | default(omit) }}", l3out: "ansible_test_l3out", external_epg: "ansible_test_external_epg", contract: "{{ fake_var | default(omit) }}", subject: "{{ fake_var | default(omit) }}", ap: "{{ fake_var | default(omit) }}", epg: "{{ fake_var | default(omit) }}", esg: "{{ fake_var | default(omit) }}"} + - { l2out: "{{ fake_var | default(omit) }}", l3out: "ansible_test_l3out", external_epg: "ansible_test_external_epg", contract: "ansible_test_contract", subject: "{{ fake_var | default(omit) }}", ap: "{{ fake_var | default(omit) }}", epg: "{{ fake_var | default(omit) }}", esg: "{{ fake_var | default(omit) }}"} + - { l2out: "{{ fake_var | default(omit) }}", l3out: "{{ fake_var | default(omit) }}", external_epg: "{{ fake_var | default(omit) }}", contract: "{{ fake_var | default(omit) }}", subject: "{{ fake_var | default(omit) }}", ap: "ansible_test_ap", epg: "ansible_test_epg", esg: "{{ fake_var | default(omit) }}"} + - { l2out: "{{ fake_var | default(omit) }}", l3out: "{{ fake_var | default(omit) }}", external_epg: "{{ fake_var | default(omit) }}", contract: "ansible_test_contract", subject: "{{ fake_var | default(omit) }}", ap: "ansible_test_ap", epg: "ansible_test_epg", esg: "{{ fake_var | default(omit) }}"} + loop_control: + loop_var: parent_class + + - name: Execute tests for esg class which is only supported in 5+ + ansible.builtin.include_tasks: subject_label.yml + loop: + - { l2out: "{{ fake_var | default(omit) }}", l3out: "{{ fake_var | default(omit) }}", external_epg: "{{ fake_var | default(omit) }}", contract: "{{ fake_var | default(omit) }}", subject: "{{ fake_var | default(omit) }}", ap: "ansible_test_ap", epg: "{{ fake_var | default(omit) }}", esg: "ansible_test_esg"} + loop_control: + loop_var: parent_class + when: version.current.0.topSystem.attributes.version is version('5', '>=') + + # ERROR TESTS + - name: Mutually exclusive all provided (error) + cisco.aci.aci_subject_label: + <<: *aci_tenant_present + l2out: ansible_test_l2out + l3out: ansible_test_l3out + subject: ansible_test_subject + epg: ansible_test_epg + esg: ansible_test_esg + register: err_mutually_exclusive_all + ignore_errors: true + + - name: Mutually exclusive two provided (error) + cisco.aci.aci_subject_label: + <<: *aci_tenant_present + l2out: ansible_test_l2out + l3out: ansible_test_l3out + register: err_mutually_exclusive_two + ignore_errors: true + + - name: Mutually exclusive esg with contract provided (error) + cisco.aci.aci_subject_label: + <<: *aci_tenant_present + esg: ansible_test_esg + contract: ansible_test_contract + register: err_mutually_exclusive_esg + ignore_errors: true + + - name: Missing required input one of L2Out, L3Out, EPG, ESG, subject (error) + cisco.aci.aci_subject_label: + <<: *aci_tenant_present + subject_label: ansible_test_l2out + subject_label_type: consumer + register: err_missing_required_input_missing_one_of + ignore_errors: true + + - name: Missing required input subject_type (error) + cisco.aci.aci_subject_label: + <<: *aci_tenant_present + l2out: ansible_test_l2out + register: err_missing_required_input_subject_type + ignore_errors: true + + - name: Missing required input subject_label (error) + cisco.aci.aci_subject_label: + <<: *aci_tenant_present + l2out: ansible_test_l2out + subject_label_type: consumer + register: err_missing_required_input_subject_label + ignore_errors: true + + - name: Missing required input external_epg (error) + cisco.aci.aci_subject_label: + <<: *aci_tenant_present + l2out: ansible_test_l2out + subject_label_type: consumer + subject_label: ansible_test_subject_label + register: err_missing_required_input_external_epg + ignore_errors: true + + - name: Assert input errors + ansible.builtin.assert: + that: + - err_mutually_exclusive_all is failed + - err_mutually_exclusive_all.msg == "parameters are mutually exclusive{{":"}} l2out|l3out|epg|esg|subject" + - err_mutually_exclusive_two is failed + - err_mutually_exclusive_two.msg == "parameters are mutually exclusive{{":"}} l2out|l3out|epg|esg|subject" + - err_mutually_exclusive_esg is failed + - err_mutually_exclusive_esg.msg == "parameters are mutually exclusive{{":"}} esg|contract" + - err_missing_required_input_missing_one_of is failed + - err_missing_required_input_missing_one_of.msg == "state is present but any of the following are missing{{":"}} l2out, l3out, epg, esg, subject" + - err_missing_required_input_subject_type is failed + - err_missing_required_input_subject_type.msg == "missing required arguments{{":"}} subject_label_type" + - err_missing_required_input_subject_label is failed + - err_missing_required_input_subject_label.msg == "state is present but all of the following are missing{{":"}} subject_label" + - err_missing_required_input_external_epg is failed + - err_missing_required_input_external_epg.msg == "missing parameter(s) required by 'l2out'{{":"}} external_epg" + + # CLEAN TEST ENVIRONMENT + - name: Remove tenant + cisco.aci.aci_tenant: + <<: *aci_tenant_absent diff --git a/tests/integration/targets/aci_subject_label/tasks/subject_label.yml b/tests/integration/targets/aci_subject_label/tasks/subject_label.yml new file mode 100644 index 000000000..bb15cc846 --- /dev/null +++ b/tests/integration/targets/aci_subject_label/tasks/subject_label.yml @@ -0,0 +1,188 @@ +- name: Set vars + ansible.builtin.set_fact: + aci_info: &aci_info + host: "{{ aci_hostname }}" + username: "{{ aci_username }}" + password: "{{ aci_password }}" + validate_certs: '{{ aci_validate_certs | default(false) }}' + use_ssl: '{{ aci_use_ssl | default(true) }}' + use_proxy: '{{ aci_use_proxy | default(true) }}' + output_level: '{{ aci_output_level | default("debug") }}' + +# CREATE TESTS +- name: Create subject label (check mode) + cisco.aci.aci_subject_label: &subject_label_present + <<: *aci_info + tenant: ansible_test + l2out: "{{ parent_class.l2out }}" + l3out: "{{ parent_class.l3out }}" + external_epg: "{{ parent_class.external_epg }}" + contract: "{{ parent_class.contract }}" + subject: "{{ parent_class.subject }}" + ap: "{{ parent_class.ap }}" + epg: "{{ parent_class.epg }}" + esg: "{{ parent_class.esg }}" + subject_label_type: consumer + subject_label: ansible_test_subject_label_1 + check_mode: true + register: cm_create_subject_label + +- name: Create subject label + cisco.aci.aci_subject_label: + <<: *subject_label_present + register: nm_create_subject_label + +- name: Create subject label again + cisco.aci.aci_subject_label: + <<: *subject_label_present + register: nm_create_subject_label_again + +- name: Assert create subject label + ansible.builtin.assert: + that: + - cm_create_subject_label is changed + - cm_create_subject_label.previous == [] + - cm_create_subject_label.current == [] + - cm_create_subject_label.proposed.vzConsSubjLbl.attributes.name == "ansible_test_subject_label_1" + - nm_create_subject_label is changed + - nm_create_subject_label.previous == [] + - nm_create_subject_label.current.0.vzConsSubjLbl.attributes.name == "ansible_test_subject_label_1" + - nm_create_subject_label.current.0.vzConsSubjLbl.attributes.descr == "" + - nm_create_subject_label.current.0.vzConsSubjLbl.attributes.isComplement == "no" + - nm_create_subject_label.current.0.vzConsSubjLbl.attributes.tag == "yellow-green" + - nm_create_subject_label_again is not changed + - nm_create_subject_label_again.previous.0.vzConsSubjLbl.attributes.name == "ansible_test_subject_label_1" + - nm_create_subject_label_again.previous.0.vzConsSubjLbl.attributes.descr == "" + - nm_create_subject_label_again.previous.0.vzConsSubjLbl.attributes.isComplement == "no" + - nm_create_subject_label_again.previous.0.vzConsSubjLbl.attributes.tag == "yellow-green" + - nm_create_subject_label_again.current.0.vzConsSubjLbl.attributes.name == "ansible_test_subject_label_1" + - nm_create_subject_label_again.current.0.vzConsSubjLbl.attributes.descr == "" + - nm_create_subject_label_again.current.0.vzConsSubjLbl.attributes.isComplement == "no" + - nm_create_subject_label_again.current.0.vzConsSubjLbl.attributes.tag == "yellow-green" + +# UPDATE TESTS +- name: Update subject label + cisco.aci.aci_subject_label: + <<: *subject_label_present + description: changed_description + complement: true + tag: chartreuse + register: nm_update_subject_label + +- name: Assert update subject label + ansible.builtin.assert: + that: + - nm_update_subject_label is changed + - nm_update_subject_label.previous.0.vzConsSubjLbl.attributes.name == "ansible_test_subject_label_1" + - nm_update_subject_label.previous.0.vzConsSubjLbl.attributes.descr == "" + - nm_update_subject_label.previous.0.vzConsSubjLbl.attributes.isComplement == "no" + - nm_update_subject_label.previous.0.vzConsSubjLbl.attributes.tag == "yellow-green" + - nm_update_subject_label.current.0.vzConsSubjLbl.attributes.name == "ansible_test_subject_label_1" + - nm_update_subject_label.current.0.vzConsSubjLbl.attributes.descr == "changed_description" + - nm_update_subject_label.current.0.vzConsSubjLbl.attributes.isComplement == "yes" + - nm_update_subject_label.current.0.vzConsSubjLbl.attributes.tag == "chartreuse" + +# QUERY TESTS +- name: Create subject label 2 + cisco.aci.aci_subject_label: + <<: *subject_label_present + subject_label: ansible_test_subject_label_2 + tag: cadet_blue + +- name: Create provider subject label 3, 4 and 5 + cisco.aci.aci_subject_label: + <<: *subject_label_present + subject_label: "{{ item }}" + subject_label_type: provider + loop: + - ansible_test_subject_label_3 + - ansible_test_subject_label_4 + - ansible_test_subject_label_5 + register: create_provider_subject_labels + +- name: Assert create of provider subject label + ansible.builtin.assert: + that: + - create_provider_subject_labels.results.0.current.0.vzProvSubjLbl.attributes.name == "ansible_test_subject_label_3" + - create_provider_subject_labels.results.1.current.0.vzProvSubjLbl.attributes.name == "ansible_test_subject_label_4" + - create_provider_subject_labels.results.2.current.0.vzProvSubjLbl.attributes.name == "ansible_test_subject_label_5" + +- name: Query subject label + cisco.aci.aci_subject_label: + <<: *subject_label_present + register: query_one + +- name: Query all consumer subject labels + cisco.aci.aci_subject_label: + <<: *aci_info + subject_label_type: consumer + state: query + register: query_all_consumer + +- name: Query all provider subject labels + cisco.aci.aci_subject_label: + <<: *aci_info + subject_label_type: provider + state: query + register: query_all_provider + +- name: Assert query subject label + ansible.builtin.assert: + that: + - query_one is not changed + - query_one.current.0.vzConsSubjLbl.attributes.name == "ansible_test_subject_label_1" + - query_one.current.0.vzConsSubjLbl.attributes.descr == "changed_description" + - query_one.current.0.vzConsSubjLbl.attributes.isComplement == "yes" + - query_one.current.0.vzConsSubjLbl.attributes.tag == "chartreuse" + - query_all_consumer is not changed + - query_all_consumer.current | length >= 2 + - query_all_provider is not changed + - query_all_provider.current | length >= 3 + +# DELETE TESTS +- name: Delete subject label (check mode) + cisco.aci.aci_subject_label: &subject_label_absent + <<: *subject_label_present + state: absent + check_mode: true + register: cm_delete_subject_label + +- name: Delete subject label + cisco.aci.aci_subject_label: + <<: *subject_label_absent + register: nm_delete_subject_label + +- name: Delete subject label again + cisco.aci.aci_subject_label: + <<: *subject_label_absent + register: nm_delete_subject_label_again + +- name: Assert delete subject label + ansible.builtin.assert: + that: + - cm_delete_subject_label is changed + - cm_delete_subject_label.previous.0.vzConsSubjLbl.attributes.name == "ansible_test_subject_label_1" + - cm_delete_subject_label.current.0.vzConsSubjLbl.attributes.name == "ansible_test_subject_label_1" + - cm_delete_subject_label.proposed == {} + - nm_delete_subject_label is changed + - nm_delete_subject_label.previous.0.vzConsSubjLbl.attributes.name == "ansible_test_subject_label_1" + - nm_delete_subject_label.current == [] + - nm_delete_subject_label_again is not changed + - nm_delete_subject_label_again.previous == [] + - nm_delete_subject_label_again.current == [] + +# CLEAN TEST ENVIRONMENT +- name: Delete consumer subject label 2 + cisco.aci.aci_subject_label: + <<: *subject_label_absent + subject_label: ansible_test_subject_label_2 + +- name: Delete provider subject labels 3, 4 and 5 + cisco.aci.aci_subject_label: + <<: *subject_label_absent + subject_label: "{{ item }}" + subject_label_type: provider + loop: + - ansible_test_subject_label_3 + - ansible_test_subject_label_4 + - ansible_test_subject_label_5