Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Addition of a filter plugin called aci_listify to the collection which flattens nested dictionaries #517

Merged
merged 7 commits into from
Nov 27, 2023
264 changes: 264 additions & 0 deletions plugins/filter/listify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
# Copyright: (c) 2017, Ramses Smeyers <[email protected]>
shrsr marked this conversation as resolved.
Show resolved Hide resolved
# Copyright: (c) 2023, Shreyas Srish <[email protected]>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
shrsr marked this conversation as resolved.
Show resolved Hide resolved

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") }}'
"""

shrsr marked this conversation as resolved.
Show resolved Hide resolved
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: '<?xml version="1.0" encoding="UTF-8"?><imdata totalCount="1"><error code="122" text="unknown managed object class foo"/></imdata>'
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
"""


def listify(d, *keys):
for result in listify_worker(d, keys, 0, {}, ""):
yield result


def listify_worker(d, keys, depth, cache, prefix):
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) for x in v):
cache_key = prefix + k
cache_value = ','.join(v)
cache_work[cache_key] = cache_value
elif not isinstance(v, (dict, list)):
cache_key = prefix + k
cache_value = v
cache_work[cache_key] = cache_value

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,
}
2 changes: 2 additions & 0 deletions tests/integration/targets/aci_filter_listify/aliases
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# No ACI simulator yet, so not enabled
# unsupported
125 changes: 125 additions & 0 deletions tests/integration/targets/aci_filter_listify/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Test code for the ACI modules
# Copyright: (c) 2023, Shreyas Srish ([email protected])
#
# 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: 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 }}'
shrsr marked this conversation as resolved.
Show resolved Hide resolved
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.1.current.0.fvSubnet.attributes.ip == "20.20.20.1/24"
- 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"
Loading