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

Alternative filter #13

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9f0d02a
Optimizations and comments added to aci_listify filter.
velotiger Jul 27, 2020
0e8e300
In filter, omit obsolete check for dict before recursive call.
velotiger Jul 27, 2020
5c56aa6
Added Apple Desktop Services Store to files ignored by git.
velotiger Aug 3, 2020
f1dff0e
Added Apple Desktop Services Store to files ignored by git.
velotiger Aug 3, 2020
ad32ff5
Alternative filter aci_listify2 added.
velotiger Aug 3, 2020
b8aecc0
Fix typo in © notice (mail address)
velotiger Sep 23, 2020
b56e385
Bugfix: Evaluate flat key/value-Pairs before dicts and lists
velotiger Oct 5, 2020
fddcc0e
Add feature: regex filter for name attribute
velotiger Oct 6, 2020
e355fd4
Add comments to plugins/filter/aci2.py
velotiger Oct 7, 2020
031995f
Use a variable to specify the attribute in regex match
velotiger Oct 12, 2020
be9ffa6
Convert name-attribute's value to str before regex match
velotiger Oct 15, 2020
f560ced
Update description and documentation
velotiger Apr 9, 2021
57e330e
Reformat code of Ansible filter aci2.py
velotiger Sep 24, 2021
113a7c8
Improvement for filter module «aci2.py»
velotiger Oct 17, 2021
d941f3a
Lists or dicts for object instances
velotiger Oct 24, 2021
a7b9a4c
Ignore MS Visual Studio Code directory
velotiger Nov 1, 2021
1a361b5
Small optimization for filter module «aci2.py»
velotiger Feb 22, 2022
09a5d1f
Do not modify object tree in filter module «aci2.py»
velotiger Mar 20, 2022
5a66f82
Fix mutable default values in function args
velotiger Dec 3, 2022
eed6b9a
Support a list of scalars as attr value in listify filter
velotiger Nov 5, 2023
d07999d
List of scalars as attr value in old listify filter
velotiger Nov 6, 2023
2298797
Merge branch «alternate-filter» into «filter-optimize»
velotiger Nov 6, 2023
28335fc
Explicit check for scalar types as attrs in listify filters
velotiger Nov 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
# Apple Desktop Services Store
.DS_Store
# PyCharm IDE
.idea
# Vim swap files
.*.swp
# Python
*.pyc
#
*.retry
# Visual Studio Code
.vscode
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Ansible Role: aci-model
A comprehensive Ansible role to model and deploy Cisco ACI fabrics.
A comprehensive Ansible role to model and deploy Cisco ACI fabrics and a custom Ansible filter for structured data.

This role provides an abstraction layer that is convenient to use. By providing your required configuration (a structured dataset) in your inventory this role will perform the needed actions to ensure that configuration is deployd on your ACI infrastructure.
This role provides an abstraction layer that is convenient to use. By providing your required configuration (a structured dataset) in your inventory this role will perform the needed actions to ensure that configuration is deployed on your ACI infrastructure.

Using this role you can easily set up demo environment, maintain a lab or use it as the basis for your in-house ACI infrastructure. It can help you understand how ACI works while prototyping and testing. No prior Ansible or ACI knowledge is required to get started.

Expand Down Expand Up @@ -38,6 +38,14 @@ You need to configure your Ansible to find this Jinja2 filter. There are two way

Because of its general usefulness, we are looking into making this *aci_listify* filter more generic and make it part of the default Ansible filters.

#### The alternative filter plugin
The alternative filter *aci_listify2* (file: *plugins/filter/aci2.py*) is installed in the same manner as the original filter. It provides the following enhancements:

* Instances of objects can be organized in lists (as in the original filter) or dicts (new).
* You can append a regex to each key so that only key values that match the regex will appear in the output.
* This is documented in the file *plugins/filter/aci2.py* itself.

The filter does not depend on this Ansible role. It can be used in any Ansible task to extract a list of items from a structured dict. For this purpose, it suffices to install the filter. You need neither the role nor the playbook or the example inventory.

## Using the example playbook

Expand Down
16 changes: 13 additions & 3 deletions example-inventory.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -159,18 +159,28 @@ fabric01:
type: phys
bd:
- name: app_bd
subnet:
subnet:
- name: 10.10.10.1
mask: 24
scope: private
vrf: Customer01
- name: web_bd
subnet:
subnet:
- name: 20.20.20.1
mask: 24
scope: public
vrf: Customer01
l3out:
l3out:
- name: l3out
- name: web_bd1
subnet:
- name: 20.20.21.1
mask: 24
scope:
- public
- shared
vrf: Customer01
l3out:
- name: l3out
vrf:
- name: Customer01
Expand Down
83 changes: 57 additions & 26 deletions plugins/filter/aci.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,69 @@
# Copyright: (c) 2017, Ramses Smeyers <rsmeyers@cisco.com>

# Copyright: (c) 2020-2023, Tilmann Boess <tilmann.boess@hr.de>
# Based on: (c) 2017, Ramses Smeyers <rsmeyers@cisco.com>
# 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


def listify(d, *keys):
return listify_worker(d, keys, 0, [], {}, '')

def listify_worker(d, keys, depth, result, 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 not isinstance(v, dict) and not isinstance(v, list):
cache_key = prefix + k
cache_value = v
cache_work[cache_key] = cache_value

if len(keys)-1 == depth :
result.append(cache_work)
else:
for k,v in item.items():
if k == keys[depth+1]:
if isinstance(v, dict) or isinstance(v, list):
result = listify_worker({k:v}, keys, depth+1, result, cache_work, prefix)
return result
"""Extract key/value data from ACI-model object tree.
The object tree must have the following structure in order to find matches: dict
at top-level, list at 2nd level and then alternating dicts and lists.
The keys must match dict names along a path in this tree down to dict that
contains at least 1 key/value pair.
Along this path all key/value pairs for all keys given are fetched.
Args:
- d (dict): object tree.
- *keys: key names to look for in ' d' in hierarchical order (the keys must form
a path in the object tree).
Returns:
- list of dicts (key/value-pairs); given keys are concatenated with '_' to form
a single key. Example: ('tenant' , 'app' , 'epg') results in 'tenant_app_epg'.
"""

def listify_worker(d, keys, depth=0, result=[], cache={}, prefix=''):
"""Recursive inner function to encapsulate the internal arguments.
Args:
- d (dict): subtree of objects for key search (depends on value of ' depth' ).
- keys (list): list of keys.
- depth (int): index (corresponding to depth in object tree) of key in key list.
- result (list): current result list of key/value-pairs.
- cache (dict): collects key/value pairs common for all items in result list.
- prefix (str): current prefix for key list in result.
"""
prefix = ''.join((prefix, keys[depth], '_'))
if keys[depth] in d:
# At level of dict.
for item in d[keys[depth]]:
# One level below: Loop thru the list in this dict. If 'd[keys[depth]]' is a dict, the next test will fail and the recursion ends.
if isinstance(item, dict):
# 'cache_work' holds all key/value pairs along the path.
cache_work = cache.copy()
for k, v in item.items():
# Two levels below: Loop thru the dict.
if isinstance(v, (str, int, float, bool, bytes)) or isinstance(v, list) and all(isinstance(x, (str, int, float, bool, bytes)) for x in v):
# Key/value found. Accept a scalar or a list of scalars as attribute value.
cache_work[''.join((prefix, k))] = v
if len(keys)-1 == depth:
# Max. depth reached.
result.append(cache_work)
else:
# Lookup next key in the dict below.
nextkey = keys[depth+1]
if nextkey in item and isinstance(item[nextkey], list):
# It's useless to test for dict here because the recursion will end in the next level.
result = listify_worker({nextkey: item[nextkey]}, keys, depth+1, result, cache_work, prefix)
return result
# End of inner function

return listify_worker(d, keys)


class FilterModule(object):
''' Ansible core jinja2 filters '''

def filters(self):
return {
'aci_listify': listify,
'aci_listify': listify
}
205 changes: 205 additions & 0 deletions plugins/filter/aci2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# Copyright: (c) 2020-2023, Tilmann Boess <tilmann.boess@hr.de>
# Based on: (c) 2017, Ramses Smeyers <rsmeyers@cisco.com>

# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

"""
This is an alternative filter to the original 'aci_listify' in 'aci.py'.
The instances (e.g. tenant, vrf, leafid, …) can be organized in a dict as
well as in a list. If you need to lookup instances directly (e.g. by other
filters), it might be useful to organize your inventory in dicts instead
of lists.

*** Examples ***
1. Simple static specification:
loop: "{{ aci_topology|aci_listify2('access_policy', 'interface_policy_profile=.+998', 'interface_selector') }}"
All paths in the output match interface policy profiles that end in «998».
E.g. interface selectors below a non-matching interface policy profile
will be suppressed from the output.
2. Dynamic specification:
loop: "{{ LEAFID_ROOT|aci_listify2(leaf_match, port_match, 'type=switch_port') }}"
vars:
leaf_match: "leafid={{ outer.leafid_Name }}"
port_match: "port={{ outer.leafid_port_Name }}"
Here the regex's for the leafid and the port are determined at runtime in an
outer task. The outer task sets the dict 'outer' and this dict is referenced
here.
'LEAFID_ROOT' is the dict in which to look for the following hierarchy:
leafid:
# leafid 101: all instances organized in lists.
- Name: 101
port:
- Name: 1
type:
- Name: vpc
- Name: 2
type:
- Name: port_channel
- Name: 3
type:
- Name: switch_port
- Name: 102
# leafid 102: organized in dicts and lists.
port:
# port instances: dict
1:
Name: 1
type:
# type instances: dict
vpc:
Name: vpc
2:
Name: 2
type:
# type instances: dict
port_channel:
Name: port_channel
4:
Name: 4
type:
# type instances: list
- Name: switch_port
( … and so on for all leaf-switches and ports …)
This matches only if:
* leafid matches the leafid delivered by the outer task.
* port matches the port delivered by the outer task.
* The port shall be configured as a simple switchport (not a channel).
The outer task could be:
- name: "example outer task"
include_tasks:
file: inner.yaml
loop: "{{ portlist }}"
loop_control:
loop_var: outer
vars:
portlist:
- leafid_Name: '10.'
leafid_port_Name: '3'
- leafid_Name: '.0.'
leafid_port_Name: '(2|4)'
The dict 'portlist' need not be specified here as task variable.
You can provide it as extra var on the command line and thus specify
dynamically which ports shall be configured.
"""

from __future__ import absolute_import, division, print_function
__metaclass__ = type

import re


def def_filter():
"""Outer function that defines the filter and encapsulates initialization of variables.
"""
# Name of the attribute used as «Name». We use uppercase «Name» to
# let it appear 1st if YAML/JSON files are sorted by keys.
# Change it to your liking.
nameAttr = 'Name'
# Regex to separate object and instance names.
rValue = re.compile('([^=]+)=(.*)')

def lister(myDict, *myKeys):
"""Extract key/value data from ACI-model object tree.
The keys must match dict names along a path in this tree down to a dict that
contains at least 1 key/value pair.
Along this path all key/value pairs for all keys given are fetched.
Args:
* myDict (dict): object tree.
* *myKeys: key names to look for in 'myDict' in hierarchical order (the keys
must form a path in the object tree).
* You can append a regex to each key (separated by «=»). Only keys
whose name-attribute matches the regex will be included in the result.
If the regex is omitted, all keys will be included (backwards compatible).
Returns:
* list of dicts (key/value-pairs); given keys are concatenated with '_' to form
a single key. Example: ('tenant' , 'app' , 'epg') results in 'tenant_app_epg'.
"""
# keyList will be a copy of the initial list «myKeys».
keyList = []
# List of regex to match the name attributes.
regexList = []
for K in myKeys:
match = rValue.fullmatch(K)
if match:
keyList.append(match.group(1))
regexList.append(re.compile(match.group(2)))
else:
keyList.append(K)
regexList.append(None)

def worker(itemList, depth, result, cache, prefix):
"""Inner function for instance evaluation.
Args:
* itemList (list): current instance list in tree (list of dicts, each item
is an ACI object).
* depth (int): index (corresponding to depth in object tree) of key in key list.
* result (list): current result list of key/value-pairs.
* cache (dict): collects key/value pairs common for all items in result list.
* prefix (str): current prefix for key list in result.
"""
for item in itemList:
# Save name attribute for later usage.
# If name attribute is missing, set to None.
name = str(item.get(nameAttr, None))
# cache holds the pathed keys (build from the key list).
# Each recursive call gets its own copy.
subcache = cache.copy()
for subKey, subItem in list(item.items()):
if isinstance(subItem, (str, int, float, bool, bytes)) or isinstance(subItem, list) and all(isinstance(x, (str, int, float, bool, bytes)) for x in subItem):
# Key/value found. Accept a scalar or a list of scalars as attribute value.
subcache['%s%s' % (prefix, subKey)] = subItem
# All key/value pairs are evaluated before dicts and lists.
# Otherwise, some attributes might not be transferred from the
# cache to the result list.
if regexList[depth] is not None and (name is None or not regexList[depth].fullmatch(name)):
# If regex was specified and the nameAttr does not match, do
# not follow the path but continue with next item. Also a
# non-existing nameAttr attribute is interpreted as non-match.
continue
result = finder(item, depth, result, subcache, prefix)
return result

def finder(objDict, depth=-1, result=None, cache=None, prefix=''):
"""Inner function for tree traversal.
* objDict (dict): current subtree, top key is name of an ACI object type.
* depth (int): index (corresponding to depth in object tree) of key in key list.
* result (list): current result list of key/value-pairs.
* cache (dict): collects key/value pairs common for all items in result list.
* prefix (str): current prefix for key list in result.
"""
if result is None:
result = []
if cache is None:
cache = {}
depth += 1
if depth == len(keyList):
# At end of key list: transfer cache to result list.
result.append(cache)
else:
prefix = ''.join((prefix, keyList[depth], '_'))
# Check if object type is in tree at given depth.
if keyList[depth] in objDict:
# Prepare item list. ACI objects may be stored as list or dict.
if isinstance(objDict[keyList[depth]], list):
itemList = objDict[keyList[depth]]
elif isinstance(objDict[keyList[depth]], dict):
itemList = list(objDict[keyList[depth]].values())
else:
# Neither dict nor list – return to upper level.
return result
result = worker(itemList, depth, result, cache.copy(), prefix)
return result

# End of function: lister
return finder(myDict)

# End of function: def_filter
return lister


class FilterModule(object):
"""Jinja2 filters for Ansible."""

def filters(self):
"""Name the filter: aci_listify2"""
return {'aci_listify2': def_filter()}