diff --git a/plugins/filter/listify.py b/plugins/filter/listify.py new file mode 100644 index 000000000..13d280937 --- /dev/null +++ b/plugins/filter/listify.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Ramses Smeyers +# Copyright: (c) 2023, Shreyas Srish +# GNU General Public License v3.0+ (see COPYING 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""" + name: aci_listify + short_description: Flattens the nested dictionaries representing the ACI model data. + description: + - This filter flattens and transforms the input data into a list. + - See the Examples section below. + options: + data: + description: This option represents the ACI model data which is a list of dictionaries or a dictionary with any level of nesting data. + type: raw + required: True + keys: + description: Comma separated keys of type string denoting the ACI objects. + required: True +""" + +EXAMPLES = r""" +- name: Set vars + ansible.builtin.set_fact: + data: + tenant: + - name: ansible_test + description: Created using listify + app: + - name: app_test + epg: + - name: web + bd: web_bd + - name: app + bd: app_bd + bd: + - name: bd_test + subnet: + - name: 10.10.10.1 + mask: 24 + scope: + - public + - shared + vrf: vrf_test + - name: bd_test2 + subnet: + - name: 20.20.20.1 + mask: 24 + scope: public + vrf: vrf_test + vrf: + - name: vrf_test + +- name: Create tenants + cisco.aci.aci_tenant: + host: apic + username: admin + password: SomeSecretPassword + tenant: '{{ item.tenant_name }}' + description: '{{ item.tenant_description }}' + with_items: '{{ data|cisco.aci.aci_listify("tenant") }}' + +- name: Create VRFs + cisco.aci.aci_vrf: + host: apic + username: admin + password: SomeSecretPassword + tenant: '{{ item.tenant_name }}' + vrf_name: '{{ item.tenant_vrf_name }}' + with_items: '{{ data|cisco.aci.aci_listify("tenant","vrf") }}' + +- name: Create BDs + cisco.aci.aci_bd: + host: apic + username: admin + password: SomeSecretPassword + tenant: '{{ item.tenant_name }}' + vrf: '{{ item.tenant_bd_vrf }}' + bd: '{{ item.tenant_bd_name }}' + enable_routing: yes + with_items: '{{ data|cisco.aci.aci_listify("tenant","bd") }}' + +- name: Create BD subnets + cisco.aci.aci_bd_subnet: + host: apic + username: admin + password: SomeSecretPassword + tenant: '{{ item.tenant_name }}' + bd: '{{ item.tenant_bd_name }}' + gateway: '{{ item.tenant_bd_subnet_name }}' + mask: '{{ item.tenant_bd_subnet_mask }}' + scope: '{{ item.tenant_bd_subnet_scope }}' + with_items: '{{ data|cisco.aci.aci_listify("tenant","bd","subnet") }}' + +- name: Create APs + cisco.aci.aci_ap: + host: apic + username: admin + password: SomeSecretPassword + tenant: '{{ item.tenant_name }}' + app_profile: '{{ item.tenant_app_name }}' + with_items: '{{ data|cisco.aci.aci_listify("tenant","app") }}' + +- name: Create EPGs + cisco.aci.aci_epg: + host: apic + username: admin + password: SomeSecretPassword + tenant: '{{ item.tenant_name }}' + app_profile: '{{ item.tenant_app_name }}' + epg: '{{ item.tenant_app_epg_name }}' + bd: '{{ item.tenant_app_epg_bd }}' + with_items: '{{ data|cisco.aci.aci_listify("tenant","app","epg") }}' +""" + +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 +""" + + +# This function takes a dictionary and a series of keys, +# and returns a list of dictionaries using recursive helper function 'listify_worker' +def listify(d, *keys): + return list(listify_worker(d, keys, 0, {}, "")) + + +# This function walks through a dictionary 'd', depth-first, +# using the keys provided, and generates a new dictionary for each key:value pair encountered +def listify_worker(d, keys, depth, cache, prefix): + # The prefix in the code is used to store the path of keys traversed in the nested dictionary, + # which helps to generate unique keys for each value when flattening the dictionary. + prefix += keys[depth] + "_" + + if keys[depth] in d: + for item in d[keys[depth]]: + cache_work = cache.copy() + if isinstance(item, dict): + for k, v in item.items(): + if isinstance(v, list) and all(isinstance(x, (str, int, float, bool, bytes)) for x in v) or not isinstance(v, (dict, list)): + # The cache in this code is a temporary storage that holds key-value pairs as the function navigates through the nested dictionary. + # It helps to generate the final output by remembering the traversed path in each recursive call. + cache_key = prefix + k + cache_value = v + cache_work[cache_key] = cache_value + # If we're at the deepest level of keys + if len(keys) - 1 == depth: + yield cache_work + else: + for k, v in item.items(): + if k == keys[depth + 1] and isinstance(v, (dict, list)): + for result in listify_worker({k: v}, keys, depth + 1, cache_work, prefix): + yield result + + +class FilterModule(object): + """Ansible core jinja2 filters""" + + def filters(self): + return { + "aci_listify": listify, + } diff --git a/tests/integration/targets/aci_filter_listify/aliases b/tests/integration/targets/aci_filter_listify/aliases new file mode 100644 index 000000000..209b793f9 --- /dev/null +++ b/tests/integration/targets/aci_filter_listify/aliases @@ -0,0 +1,2 @@ +# No ACI simulator yet, so not enabled +# unsupported diff --git a/tests/integration/targets/aci_filter_listify/tasks/main.yml b/tests/integration/targets/aci_filter_listify/tasks/main.yml new file mode 100644 index 000000000..e0ab7fe81 --- /dev/null +++ b/tests/integration/targets/aci_filter_listify/tasks/main.yml @@ -0,0 +1,148 @@ +# Test code for the ACI modules +# Copyright: (c) 2023, Shreyas Srish (shrsr@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 + 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") }}' + aci_model_data: + tenant: + - name: ansible_test + description: Created using listify + app: + - name: app_test + epg: + - name: web + bd: web_bd + - name: app + bd: app_bd + bd: + - name: bd_test + subnet: + - name: 10.10.10.1 + mask: 24 + scope: + - private + - shared + vrf: vrf_test + - name: bd_test2 + subnet: + - name: 20.20.20.1 + mask: 24 + scope: [public, shared] + vrf: vrf_test + vrf: + - name: vrf_test + - name: ansible_test2 + description: Created using listify + app: + - name: app_test2 + epg: + - name: web2 + bd: web_bd2 + - name: app2 + bd: app_bd2 + +- name: Verify Cloud and Non-Cloud Sites in use. + include_tasks: ../../../../../../integration/targets/aci_cloud_provider/tasks/main.yml + +- name: Execute tasks only for non-cloud sites + when: query_cloud.current == [] # This condition will execute only non-cloud sites + block: # block specifies execution of tasks within, based on conditions + + - name: Create tenants + cisco.aci.aci_tenant: + <<: *aci_info + tenant: '{{ item.tenant_name }}' + description: '{{ item.tenant_description }}' + with_items: '{{ aci_model_data|cisco.aci.aci_listify("tenant") }}' + register: tenant_listify + + - name: Create VRFs + cisco.aci.aci_vrf: + <<: *aci_info + tenant: '{{ item.tenant_name }}' + vrf_name: '{{ item.tenant_vrf_name }}' + with_items: '{{ aci_model_data|cisco.aci.aci_listify("tenant","vrf") }}' + register: vrf_listify + + - name: Create BDs + cisco.aci.aci_bd: + <<: *aci_info + tenant: '{{ item.tenant_name }}' + vrf: '{{ item.tenant_bd_vrf }}' + bd: '{{ item.tenant_bd_name }}' + enable_routing: yes + with_items: '{{ aci_model_data|cisco.aci.aci_listify("tenant","bd") }}' + register: bd_listify + + - name: Create BD subnets + cisco.aci.aci_bd_subnet: + <<: *aci_info + tenant: '{{ item.tenant_name }}' + bd: '{{ item.tenant_bd_name }}' + gateway: '{{ item.tenant_bd_subnet_name }}' + mask: '{{ item.tenant_bd_subnet_mask }}' + scope: '{{ item.tenant_bd_subnet_scope }}' + with_items: '{{ aci_model_data|cisco.aci.aci_listify("tenant","bd","subnet") }}' + register: bd_subnets_listify + + - name: Create APs + cisco.aci.aci_ap: + <<: *aci_info + tenant: '{{ item.tenant_name }}' + app_profile: '{{ item.tenant_app_name }}' + with_items: '{{ aci_model_data|cisco.aci.aci_listify("tenant","app") }}' + register: ap_listify + + - name: Create EPGs + cisco.aci.aci_epg: + <<: *aci_info + tenant: '{{ item.tenant_name }}' + app_profile: '{{ item.tenant_app_name }}' + epg: '{{ item.tenant_app_epg_name }}' + bd: '{{ item.tenant_app_epg_bd }}' + with_items: '{{ aci_model_data|cisco.aci.aci_listify("tenant","app","epg") }}' + register: epg_listify + + - name: Validate listify + assert: + that: + - tenant_listify.results.0.current.0.fvTenant.attributes.name == "ansible_test" + - vrf_listify.results.0.current.0.fvCtx.attributes.name == "vrf_test" + - bd_listify.results.0.current.0.fvBD.attributes.name == "bd_test" + - bd_listify.results.1.current.0.fvBD.attributes.name == "bd_test2" + - bd_subnets_listify.results.0.current.0.fvSubnet.attributes.ip == "10.10.10.1/24" + - bd_subnets_listify.results.0.current.0.fvSubnet.attributes.scope == "private,shared" + - bd_subnets_listify.results.1.current.0.fvSubnet.attributes.ip == "20.20.20.1/24" + - bd_subnets_listify.results.1.current.0.fvSubnet.attributes.scope == "public,shared" + - ap_listify.results.0.current.0.fvAp.attributes.name == "app_test" + - epg_listify.results.0.current.0.fvAEPg.attributes.name == "web" + - epg_listify.results.1.current.0.fvAEPg.attributes.name == "app" + - tenant_listify.results.1.current.0.fvTenant.attributes.name == "ansible_test2" + - ap_listify.results.1.current.0.fvAp.attributes.name == "app_test2" + - epg_listify.results.2.current.0.fvAEPg.attributes.name == "web2" + - epg_listify.results.3.current.0.fvAEPg.attributes.name == "app2" + + # Clean Up Environment + - name: Delete tenants + cisco.aci.aci_tenant: + <<: *aci_info + tenant: "{{ item }}" + state: absent + loop: + - ansible_test + - ansible_test2