Add index_of plugin (#6)
* Add index_of filter/lookup plugin * Update README * Fix unittest import, black * Remove true/false tests as ansible 2.9 uses jinja 2.10, need 2.11 * Add jinja > 2.11 to unit test requirements * Reformat requirements file * Limit unit to tests available in < jinja 2.11 * Reblack * Remove float (j 2.11) * Add integration tests, update docstring * Comment out test using integer, requires jinja 2.11 * doc updates Co-authored-by: cidrblock <brad@thethorntons.net>pull/8/head
parent
309ccb5563
commit
197b9d93b9
|
@ -21,13 +21,15 @@ PEP440 is the schema used to describe the versions of Ansible.
|
|||
### Filter plugins
|
||||
Name | Description
|
||||
--- | ---
|
||||
ansible.utils.get_path|Get the value within a variable using a path. [See examples](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.get_path_lookup.rst)
|
||||
ansible.utils.to_paths|Convert complex objects to paths. [See examples](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.to_paths_lookup.rst)
|
||||
ansible.utils.get_path|Get value using path. [See examples](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.get_path_lookup.rst)
|
||||
ansible.utils.index_of|Find items in a list. [See examples](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.netcommon.index_of_lookup.rst)
|
||||
ansible.utils.to_paths|Convert objects to paths. [See examples](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.to_paths_lookup.rst)
|
||||
|
||||
### Lookup plugins
|
||||
Name | Description
|
||||
--- | ---
|
||||
[ansible.utils.get_path](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.get_path_lookup.rst)|Retrieve the value in a variable using a path
|
||||
[ansible.utils.index_of](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.index_of_lookup.rst)|Find the indicies of items in a list matching some criteria
|
||||
[ansible.utils.to_paths](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.to_paths_lookup.rst)|Flatten a complex object into a dictionary of paths and values
|
||||
|
||||
<!--end collection content-->
|
||||
|
|
|
@ -0,0 +1,504 @@
|
|||
.. _ansible.utils.index_of_lookup:
|
||||
|
||||
|
||||
**********************
|
||||
ansible.utils.index_of
|
||||
**********************
|
||||
|
||||
**Find the indicies of items in a list matching some criteria**
|
||||
|
||||
|
||||
Version added: 1.0
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
:depth: 1
|
||||
|
||||
|
||||
Synopsis
|
||||
--------
|
||||
- This lookup returns the indicies of items matching some criteria in a list
|
||||
- When working with a list of dictionaries, the key to evaluate can be specified
|
||||
- ``index_of`` is also available as a ``filter_plugin`` for convenience
|
||||
|
||||
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<table border=0 cellpadding=0 class="documentation-table">
|
||||
<tr>
|
||||
<th colspan="1">Parameter</th>
|
||||
<th>Choices/<font color="blue">Defaults</font></th>
|
||||
<th>Configuration</th>
|
||||
<th width="100%">Comments</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>_terms</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">-</span>
|
||||
/ <span style="color: red">required</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>The values below provided in the order <code>test</code>, <code>value</code>, <code>key</code>.</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>data</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">list</span>
|
||||
/ <span style="color: red">required</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>A list of items to enumerate and test against</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>fail_on_missing</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">boolean</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<ul style="margin: 0; padding: 0"><b>Choices:</b>
|
||||
<li>no</li>
|
||||
<li>yes</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>When provided a list of dictionaries, fail if the key is missing from one or more of the dictionaries</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>key</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>When the data provided is a list of dictionaries, run the test againt this dictionary key When using a <code>key</code>, the <code>data</code> must only contain dictionaries See <code>fail_on_missing</code> below to determine the behaviour when the <code>key</code> is missing from a dictionary in the <code>data</code></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>test</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">string</span>
|
||||
/ <span style="color: red">required</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>The name of the test to run against the list, a valid jinja2 test or ansible test plugin. Jinja2 includes the following tests <a href='http://jinja.palletsprojects.com/templates/#builtin-tests'>http://jinja.palletsprojects.com/templates/#builtin-tests</a>. An overview of tests included in ansible <a href='https://docs.ansible.com/ansible/latest/user_guide/playbooks_tests.html'>https://docs.ansible.com/ansible/latest/user_guide/playbooks_tests.html</a></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>value</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">raw</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>The value used to test each list item against Not required for simple tests (eg: <code>true</code>, <code>false</code>, <code>even</code>, <code>odd</code>) May be a <code>string</code>, <code>boolean</code>, <code>number</code>, <code>regular expesion</code> <code>dict</code> etc, depending on the <code>test</code> used</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="parameter-"></div>
|
||||
<b>wantlist</b>
|
||||
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">boolean</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<ul style="margin: 0; padding: 0"><b>Choices:</b>
|
||||
<li>no</li>
|
||||
<li>yes</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
<div>When only a single entry in the <code>data</code> is matched, that entries index is returned as an integer If set to <code>True</code>, the return value will always be a list, even if only a single entry is matched This can also be accomplised using <code>query</code> or <code>q</code> instead of <code>lookup</code> <a href='https://docs.ansible.com/ansible/latest/plugins/lookup.html'>https://docs.ansible.com/ansible/latest/plugins/lookup.html</a></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br/>
|
||||
|
||||
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
#### Simple examples using a list of values
|
||||
|
||||
- set_fact:
|
||||
data:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
|
||||
- name: Find the index of 2, lookup or filter
|
||||
set_fact:
|
||||
as_lookup: "{{ lookup('ansible.utils.index_of', data, 'eq', 2) }}"
|
||||
as_filter: "{{ data|ansible.utils.index_of('eq', 2) }}"
|
||||
|
||||
# TASK [Find the index of 2, lookup or filter] *******************************
|
||||
# ok: [sw01] => changed=false
|
||||
# ansible_facts:
|
||||
# as_filter: '1'
|
||||
# as_lookup: '1'
|
||||
|
||||
- name: Any test can be negated using not or !
|
||||
set_fact:
|
||||
as_lookup: "{{ lookup('ansible.utils.index_of', data, 'not in', [1,2]) }}"
|
||||
as_filter: "{{ data|ansible.utils.index_of('!in', [1,2]) }}"
|
||||
|
||||
# TASK [Any test can be negated using not or !] ******************************
|
||||
# ok: [localhost] => changed=false
|
||||
# ansible_facts:
|
||||
# as_filter: '2'
|
||||
# as_lookup: '2'
|
||||
|
||||
- name: Find the index of 2, lookup or filter, ensure list is returned
|
||||
set_fact:
|
||||
as_query: "{{ query('ansible.utils.index_of', data, 'eq', 2) }}"
|
||||
as_lookup: "{{ lookup('ansible.utils.index_of', data, 'eq', 2, wantlist=True) }}"
|
||||
as_filter: "{{ data|ansible.utils.index_of('eq', 2, wantlist=True) }}"
|
||||
|
||||
# TASK [Find the index of 2, lookup or filter, ensure list is returned] ******
|
||||
# ok: [sw01] => changed=false
|
||||
# ansible_facts:
|
||||
# as_filter:
|
||||
# - 1
|
||||
# as_lookup:
|
||||
# - 1
|
||||
# as_query:
|
||||
# - 1
|
||||
|
||||
- name: Find the index of 3 using the long format
|
||||
set_fact:
|
||||
as_query: "{{ query('ansible.utils.index_of', data=data, test='eq', value=value) }}"
|
||||
as_lookup: "{{ lookup('ansible.utils.index_of', data=data, test='eq',value =value, wantlist=True) }}"
|
||||
as_filter: "{{ data|ansible.utils.index_of(test='eq', value=value, wantlist=True) }}"
|
||||
vars:
|
||||
value: 3
|
||||
|
||||
# TASK [Find the index of 3 using the long format] ***************************
|
||||
# ok: [sw01] => changed=false
|
||||
# ansible_facts:
|
||||
# as_filter:
|
||||
# - 2
|
||||
# as_lookup:
|
||||
# - 2
|
||||
# as_query:
|
||||
# - 2
|
||||
|
||||
- name: Find numbers greater than 1, using loop
|
||||
debug:
|
||||
msg: "{{ data[item] }} is {{ test }} than {{ value }}"
|
||||
loop: "{{ data|ansible.utils.index_of(test, value) }}"
|
||||
vars:
|
||||
test: '>'
|
||||
value: 1
|
||||
|
||||
# TASK [Find numbers great than 1, using loop] *******************************
|
||||
# ok: [sw01] => (item=1) =>
|
||||
# msg: 2 is > than 1
|
||||
# ok: [sw01] => (item=2) =>
|
||||
# msg: 3 is > than 1
|
||||
|
||||
- name: Find numbers greater than 1, using with
|
||||
debug:
|
||||
msg: "{{ data[item] }} is {{ params.test }} than {{ params.value }}"
|
||||
with_ansible.utils.index_of: "{{ params }}"
|
||||
vars:
|
||||
params:
|
||||
data: "{{ data }}"
|
||||
test: '>'
|
||||
value: 1
|
||||
|
||||
# TASK [Find numbers greater than 1, using with] *****************************
|
||||
# ok: [sw01] => (item=1) =>
|
||||
# msg: 2 is > than 1
|
||||
# ok: [sw01] => (item=2) =>
|
||||
# msg: 3 is > than 1
|
||||
|
||||
|
||||
|
||||
#### Working with lists of dictionaries
|
||||
|
||||
- set_fact:
|
||||
data:
|
||||
- name: sw01.example.lan
|
||||
type: switch
|
||||
- name: rtr01.example.lan
|
||||
type: router
|
||||
- name: fw01.example.corp
|
||||
type: firewall
|
||||
- name: fw02.example.corp
|
||||
type: firewall
|
||||
|
||||
- name: Find the index of all firewalls using the type key
|
||||
set_fact:
|
||||
as_query: "{{ query('ansible.utils.index_of', data, 'eq', 'firewall', 'type') }}"
|
||||
as_lookup: "{{ lookup('ansible.utils.index_of', data, 'eq', 'firewall', 'type') }}"
|
||||
as_filter: "{{ data|ansible.utils.index_of('eq', 'firewall', 'type') }}"
|
||||
|
||||
# TASK [Find the index of all firewalls using the type key] ******************
|
||||
# ok: [sw01] => changed=false
|
||||
# ansible_facts:
|
||||
# as_filter:
|
||||
# - 2
|
||||
# - 3
|
||||
# as_lookup:
|
||||
# - 2
|
||||
# - 3
|
||||
# as_query:
|
||||
# - 2
|
||||
# - 3
|
||||
|
||||
- name: Find the index of all firewalls, use in a loop, as a filter
|
||||
debug:
|
||||
msg: "The type of {{ device_type }} at index {{ item }} has name {{ data[item].name }}."
|
||||
loop: "{{ data|ansible.utils.index_of('eq', device_type, 'type') }}"
|
||||
vars:
|
||||
device_type: firewall
|
||||
|
||||
# TASK [Find the index of all firewalls, use in a loop] **********************
|
||||
# ok: [sw01] => (item=2) =>
|
||||
# msg: The type of firewall at index 2 has name fw01.example.corp
|
||||
# ok: [sw01] => (item=3) =>
|
||||
# msg: The type of firewall at index 3 has name fw02.example.corp
|
||||
|
||||
- name: Find the index of all devices with a .corp name, as a lookup
|
||||
debug:
|
||||
msg: "The device named {{ data[item].name }} is a {{ data[item].type }}"
|
||||
loop: "{{ lookup('ansible.utils.index_of', data, 'regex', regex, 'name') }}"
|
||||
vars:
|
||||
regex: '\.corp$' # ends with .corp
|
||||
|
||||
# TASK [Find the index of all devices with a .corp name, as a lookup] **********
|
||||
# ok: [sw01] => (item=2) =>
|
||||
# msg: The device named fw01.example.corp is a firewall
|
||||
# ok: [sw01] => (item=3) =>
|
||||
# msg: The device named fw02.example.corp is a firewall
|
||||
|
||||
|
||||
|
||||
#### Working with data from resource modules
|
||||
|
||||
- name: Retrieve the current L3 interface configuration
|
||||
cisco.nxos.nxos_l3_interfaces:
|
||||
state: gathered
|
||||
register: current_l3
|
||||
|
||||
# TASK [Retrieve the current L3 interface configuration] *********************
|
||||
# ok: [sw01] => changed=false
|
||||
# gathered:
|
||||
# - name: Ethernet1/1
|
||||
# - name: Ethernet1/2
|
||||
# <...>
|
||||
# - name: Ethernet1/128
|
||||
# - ipv4:
|
||||
# - address: 192.168.101.14/24
|
||||
# name: mgmt0
|
||||
|
||||
- name: Find the index of the interface and address with a 192.168.101.xx ip address
|
||||
set_fact:
|
||||
found: "{{ found + entry }}"
|
||||
with_indexed_items: "{{ current_l3.gathered }}"
|
||||
vars:
|
||||
found: []
|
||||
ip: '192.168.101.'
|
||||
address: "{{ item.1.ipv4|d([])|ansible.utils.index_of('search', ip, 'address', wantlist=True) }}"
|
||||
entry:
|
||||
- interface_idx: "{{ item.0 }}"
|
||||
address_idxs: "{{ address }}"
|
||||
when: address
|
||||
|
||||
# TASK [debug] ***************************************************************
|
||||
# ok: [sw01] =>
|
||||
# found:
|
||||
# - address_idxs:
|
||||
# - 0
|
||||
# interface_idx: '128'
|
||||
|
||||
- name: Show all interfaces and their address
|
||||
debug:
|
||||
msg: "{{ interface.name }} has ip {{ address }}"
|
||||
loop: "{{ found|subelements('address_idxs') }}"
|
||||
vars:
|
||||
interface: "{{ current_l3.gathered[item.0.interface_idx|int] }}"
|
||||
address: "{{ interface.ipv4[item.1].address }}"
|
||||
|
||||
# TASK [debug] ***************************************************************
|
||||
# ok: [sw01] => (item=[{'interface_idx': '128', 'address_idx': [0]}, 0]) =>
|
||||
# msg: mgmt0 has ip 192.168.101.14/24
|
||||
|
||||
|
||||
|
||||
#### Working with complex structures
|
||||
|
||||
- set_fact:
|
||||
data:
|
||||
interfaces:
|
||||
interface:
|
||||
- config:
|
||||
description: configured by Ansible - 1
|
||||
enabled: True
|
||||
loopback-mode: False
|
||||
mtu: 1024
|
||||
name: loopback0000
|
||||
type: eth
|
||||
name: loopback0000
|
||||
subinterfaces:
|
||||
subinterface:
|
||||
- config:
|
||||
description: subinterface configured by Ansible - 1
|
||||
enabled: True
|
||||
index: 5
|
||||
index: 5
|
||||
- config:
|
||||
description: subinterface configured by Ansible - 2
|
||||
enabled: False
|
||||
index: 2
|
||||
index: 2
|
||||
- config:
|
||||
description: configured by Ansible - 2
|
||||
enabled: False
|
||||
loopback-mode: False
|
||||
mtu: 2048
|
||||
name: loopback1111
|
||||
type: virt
|
||||
name: loopback1111
|
||||
subinterfaces:
|
||||
subinterface:
|
||||
- config:
|
||||
description: subinterface configured by Ansible - 3
|
||||
enabled: True
|
||||
index: 10
|
||||
index: 10
|
||||
- config:
|
||||
description: subinterface configured by Ansible - 4
|
||||
enabled: False
|
||||
index: 3
|
||||
index: 3
|
||||
|
||||
|
||||
- name: Find the description of loopback111, subinterface index 10
|
||||
debug:
|
||||
msg: |-
|
||||
{{ data.interfaces.interface[int_idx|int]
|
||||
.subinterfaces.subinterface[subint_idx|int]
|
||||
.config.description }}
|
||||
vars:
|
||||
# the values to search for
|
||||
int_name: loopback1111
|
||||
sub_index: 10
|
||||
# retrieve the index in each nested list
|
||||
int_idx: |
|
||||
{{ data.interfaces.interface|
|
||||
ansible.utils.index_of('eq', int_name, 'name') }}
|
||||
subint_idx: |
|
||||
{{ data.interfaces.interface[int_idx|int]
|
||||
.subinterfaces.subinterface|
|
||||
ansible.utils.index_of('eq', sub_index, 'index') }}
|
||||
|
||||
# TASK [Find the description of loopback111, subinterface index 10] ************
|
||||
# ok: [sw01] =>
|
||||
# msg: subinterface configured by Ansible - 3
|
||||
|
||||
|
||||
|
||||
Return Values
|
||||
-------------
|
||||
Common return values are documented `here <https://docs.ansible.com/ansible/latest/reference_appendices/common_return_values.html#common-return-values>`_, the following are the fields unique to this lookup:
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<table border=0 cellpadding=0 class="documentation-table">
|
||||
<tr>
|
||||
<th colspan="1">Key</th>
|
||||
<th>Returned</th>
|
||||
<th width="100%">Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1">
|
||||
<div class="ansibleOptionAnchor" id="return-"></div>
|
||||
<b>_raw</b>
|
||||
<a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
|
||||
<div style="font-size: small">
|
||||
<span style="color: purple">-</span>
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<div>One or more zero-based indicies of the matching list items</div>
|
||||
<div>See <code>wantlist</code> if a list is always required</div>
|
||||
<br/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br/><br/>
|
||||
|
||||
|
||||
Status
|
||||
------
|
||||
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
- Bradley Thornton (@cidrblock)
|
||||
|
||||
|
||||
.. hint::
|
||||
Configuration entries for each entry type have a low to high priority order. For example, a variable that is lower in the list will override a variable that is higher up.
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2020 Red Hat
|
||||
# GNU General Public License v3.0+
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
|
||||
"""
|
||||
The index_of filter plugin
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible_collections.ansible.utils.plugins.module_utils.common.index_of import (
|
||||
index_of,
|
||||
)
|
||||
from jinja2.filters import environmentfilter
|
||||
|
||||
|
||||
@environmentfilter
|
||||
def _index_of(*args, **kwargs):
|
||||
"""Find items in a list. [See examples](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.netcommon.index_of_lookup.rst)"""
|
||||
kwargs["tests"] = args[0].tests
|
||||
args = args[1:]
|
||||
return index_of(*args, **kwargs)
|
||||
|
||||
|
||||
class FilterModule(object):
|
||||
""" index_of """
|
||||
|
||||
def filters(self):
|
||||
"""a mapping of filter names to functions"""
|
||||
return {"index_of": _index_of}
|
|
@ -0,0 +1,373 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2020 Red Hat
|
||||
# GNU General Public License v3.0+
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
|
||||
"""
|
||||
The index_of lookup plugin
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = """
|
||||
lookup: index_of
|
||||
author: Bradley Thornton (@cidrblock)
|
||||
version_added: "1.0"
|
||||
short_description: Find the indicies of items in a list matching some criteria
|
||||
description:
|
||||
- This lookup returns the indicies of items matching some criteria in a list
|
||||
- When working with a list of dictionaries, the key to evaluate can be specified
|
||||
- C(index_of) is also available as a C(filter_plugin) for convenience
|
||||
options:
|
||||
_terms:
|
||||
description: The values below provided in the order C(test), C(value), C(key).
|
||||
required: True
|
||||
data:
|
||||
description: A list of items to enumerate and test against
|
||||
type: list
|
||||
required: True
|
||||
test:
|
||||
description: >
|
||||
The name of the test to run against the list, a valid jinja2 test or ansible test plugin.
|
||||
Jinja2 includes the following tests U(http://jinja.palletsprojects.com/templates/#builtin-tests).
|
||||
An overview of tests included in ansible U(https://docs.ansible.com/ansible/latest/user_guide/playbooks_tests.html)
|
||||
type: str
|
||||
required: True
|
||||
value:
|
||||
description: >
|
||||
The value used to test each list item against
|
||||
Not required for simple tests (eg: C(true), C(false), C(even), C(odd))
|
||||
May be a C(string), C(boolean), C(number), C(regular expesion) C(dict) etc, depending on the C(test) used
|
||||
type: raw
|
||||
key:
|
||||
description: >
|
||||
When the data provided is a list of dictionaries, run the test againt this dictionary key
|
||||
When using a C(key), the C(data) must only contain dictionaries
|
||||
See C(fail_on_missing) below to determine the behaviour when the C(key) is missing from a dictionary in the C(data)
|
||||
type: str
|
||||
fail_on_missing:
|
||||
description: When provided a list of dictionaries, fail if the key is missing from one or more of the dictionaries
|
||||
type: bool
|
||||
wantlist:
|
||||
description: >
|
||||
When only a single entry in the C(data) is matched, that entries index is returned as an integer
|
||||
If set to C(True), the return value will always be a list, even if only a single entry is matched
|
||||
This can also be accomplised using C(query) or C(q) instead of C(lookup)
|
||||
U(https://docs.ansible.com/ansible/latest/plugins/lookup.html)
|
||||
type: bool
|
||||
|
||||
notes:
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
|
||||
#### Simple examples using a list of values
|
||||
|
||||
- set_fact:
|
||||
data:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
|
||||
- name: Find the index of 2, lookup or filter
|
||||
set_fact:
|
||||
as_lookup: "{{ lookup('ansible.utils.index_of', data, 'eq', 2) }}"
|
||||
as_filter: "{{ data|ansible.utils.index_of('eq', 2) }}"
|
||||
|
||||
# TASK [Find the index of 2, lookup or filter] *******************************
|
||||
# ok: [sw01] => changed=false
|
||||
# ansible_facts:
|
||||
# as_filter: '1'
|
||||
# as_lookup: '1'
|
||||
|
||||
- name: Any test can be negated using not or !
|
||||
set_fact:
|
||||
as_lookup: "{{ lookup('ansible.utils.index_of', data, 'not in', [1,2]) }}"
|
||||
as_filter: "{{ data|ansible.utils.index_of('!in', [1,2]) }}"
|
||||
|
||||
# TASK [Any test can be negated using not or !] ******************************
|
||||
# ok: [localhost] => changed=false
|
||||
# ansible_facts:
|
||||
# as_filter: '2'
|
||||
# as_lookup: '2'
|
||||
|
||||
- name: Find the index of 2, lookup or filter, ensure list is returned
|
||||
set_fact:
|
||||
as_query: "{{ query('ansible.utils.index_of', data, 'eq', 2) }}"
|
||||
as_lookup: "{{ lookup('ansible.utils.index_of', data, 'eq', 2, wantlist=True) }}"
|
||||
as_filter: "{{ data|ansible.utils.index_of('eq', 2, wantlist=True) }}"
|
||||
|
||||
# TASK [Find the index of 2, lookup or filter, ensure list is returned] ******
|
||||
# ok: [sw01] => changed=false
|
||||
# ansible_facts:
|
||||
# as_filter:
|
||||
# - 1
|
||||
# as_lookup:
|
||||
# - 1
|
||||
# as_query:
|
||||
# - 1
|
||||
|
||||
- name: Find the index of 3 using the long format
|
||||
set_fact:
|
||||
as_query: "{{ query('ansible.utils.index_of', data=data, test='eq', value=value) }}"
|
||||
as_lookup: "{{ lookup('ansible.utils.index_of', data=data, test='eq',value =value, wantlist=True) }}"
|
||||
as_filter: "{{ data|ansible.utils.index_of(test='eq', value=value, wantlist=True) }}"
|
||||
vars:
|
||||
value: 3
|
||||
|
||||
# TASK [Find the index of 3 using the long format] ***************************
|
||||
# ok: [sw01] => changed=false
|
||||
# ansible_facts:
|
||||
# as_filter:
|
||||
# - 2
|
||||
# as_lookup:
|
||||
# - 2
|
||||
# as_query:
|
||||
# - 2
|
||||
|
||||
- name: Find numbers greater than 1, using loop
|
||||
debug:
|
||||
msg: "{{ data[item] }} is {{ test }} than {{ value }}"
|
||||
loop: "{{ data|ansible.utils.index_of(test, value) }}"
|
||||
vars:
|
||||
test: '>'
|
||||
value: 1
|
||||
|
||||
# TASK [Find numbers great than 1, using loop] *******************************
|
||||
# ok: [sw01] => (item=1) =>
|
||||
# msg: 2 is > than 1
|
||||
# ok: [sw01] => (item=2) =>
|
||||
# msg: 3 is > than 1
|
||||
|
||||
- name: Find numbers greater than 1, using with
|
||||
debug:
|
||||
msg: "{{ data[item] }} is {{ params.test }} than {{ params.value }}"
|
||||
with_ansible.utils.index_of: "{{ params }}"
|
||||
vars:
|
||||
params:
|
||||
data: "{{ data }}"
|
||||
test: '>'
|
||||
value: 1
|
||||
|
||||
# TASK [Find numbers greater than 1, using with] *****************************
|
||||
# ok: [sw01] => (item=1) =>
|
||||
# msg: 2 is > than 1
|
||||
# ok: [sw01] => (item=2) =>
|
||||
# msg: 3 is > than 1
|
||||
|
||||
|
||||
|
||||
#### Working with lists of dictionaries
|
||||
|
||||
- set_fact:
|
||||
data:
|
||||
- name: sw01.example.lan
|
||||
type: switch
|
||||
- name: rtr01.example.lan
|
||||
type: router
|
||||
- name: fw01.example.corp
|
||||
type: firewall
|
||||
- name: fw02.example.corp
|
||||
type: firewall
|
||||
|
||||
- name: Find the index of all firewalls using the type key
|
||||
set_fact:
|
||||
as_query: "{{ query('ansible.utils.index_of', data, 'eq', 'firewall', 'type') }}"
|
||||
as_lookup: "{{ lookup('ansible.utils.index_of', data, 'eq', 'firewall', 'type') }}"
|
||||
as_filter: "{{ data|ansible.utils.index_of('eq', 'firewall', 'type') }}"
|
||||
|
||||
# TASK [Find the index of all firewalls using the type key] ******************
|
||||
# ok: [sw01] => changed=false
|
||||
# ansible_facts:
|
||||
# as_filter:
|
||||
# - 2
|
||||
# - 3
|
||||
# as_lookup:
|
||||
# - 2
|
||||
# - 3
|
||||
# as_query:
|
||||
# - 2
|
||||
# - 3
|
||||
|
||||
- name: Find the index of all firewalls, use in a loop, as a filter
|
||||
debug:
|
||||
msg: "The type of {{ device_type }} at index {{ item }} has name {{ data[item].name }}."
|
||||
loop: "{{ data|ansible.utils.index_of('eq', device_type, 'type') }}"
|
||||
vars:
|
||||
device_type: firewall
|
||||
|
||||
# TASK [Find the index of all firewalls, use in a loop] **********************
|
||||
# ok: [sw01] => (item=2) =>
|
||||
# msg: The type of firewall at index 2 has name fw01.example.corp
|
||||
# ok: [sw01] => (item=3) =>
|
||||
# msg: The type of firewall at index 3 has name fw02.example.corp
|
||||
|
||||
- name: Find the index of all devices with a .corp name, as a lookup
|
||||
debug:
|
||||
msg: "The device named {{ data[item].name }} is a {{ data[item].type }}"
|
||||
loop: "{{ lookup('ansible.utils.index_of', data, 'regex', regex, 'name') }}"
|
||||
vars:
|
||||
regex: '\.corp$' # ends with .corp
|
||||
|
||||
# TASK [Find the index of all devices with a .corp name, as a lookup] **********
|
||||
# ok: [sw01] => (item=2) =>
|
||||
# msg: The device named fw01.example.corp is a firewall
|
||||
# ok: [sw01] => (item=3) =>
|
||||
# msg: The device named fw02.example.corp is a firewall
|
||||
|
||||
|
||||
|
||||
#### Working with data from resource modules
|
||||
|
||||
- name: Retrieve the current L3 interface configuration
|
||||
cisco.nxos.nxos_l3_interfaces:
|
||||
state: gathered
|
||||
register: current_l3
|
||||
|
||||
# TASK [Retrieve the current L3 interface configuration] *********************
|
||||
# ok: [sw01] => changed=false
|
||||
# gathered:
|
||||
# - name: Ethernet1/1
|
||||
# - name: Ethernet1/2
|
||||
# <...>
|
||||
# - name: Ethernet1/128
|
||||
# - ipv4:
|
||||
# - address: 192.168.101.14/24
|
||||
# name: mgmt0
|
||||
|
||||
- name: Find the index of the interface and address with a 192.168.101.xx ip address
|
||||
set_fact:
|
||||
found: "{{ found + entry }}"
|
||||
with_indexed_items: "{{ current_l3.gathered }}"
|
||||
vars:
|
||||
found: []
|
||||
ip: '192.168.101.'
|
||||
address: "{{ item.1.ipv4|d([])|ansible.utils.index_of('search', ip, 'address', wantlist=True) }}"
|
||||
entry:
|
||||
- interface_idx: "{{ item.0 }}"
|
||||
address_idxs: "{{ address }}"
|
||||
when: address
|
||||
|
||||
# TASK [debug] ***************************************************************
|
||||
# ok: [sw01] =>
|
||||
# found:
|
||||
# - address_idxs:
|
||||
# - 0
|
||||
# interface_idx: '128'
|
||||
|
||||
- name: Show all interfaces and their address
|
||||
debug:
|
||||
msg: "{{ interface.name }} has ip {{ address }}"
|
||||
loop: "{{ found|subelements('address_idxs') }}"
|
||||
vars:
|
||||
interface: "{{ current_l3.gathered[item.0.interface_idx|int] }}"
|
||||
address: "{{ interface.ipv4[item.1].address }}"
|
||||
|
||||
# TASK [debug] ***************************************************************
|
||||
# ok: [sw01] => (item=[{'interface_idx': '128', 'address_idx': [0]}, 0]) =>
|
||||
# msg: mgmt0 has ip 192.168.101.14/24
|
||||
|
||||
|
||||
|
||||
#### Working with complex structures
|
||||
|
||||
- set_fact:
|
||||
data:
|
||||
interfaces:
|
||||
interface:
|
||||
- config:
|
||||
description: configured by Ansible - 1
|
||||
enabled: True
|
||||
loopback-mode: False
|
||||
mtu: 1024
|
||||
name: loopback0000
|
||||
type: eth
|
||||
name: loopback0000
|
||||
subinterfaces:
|
||||
subinterface:
|
||||
- config:
|
||||
description: subinterface configured by Ansible - 1
|
||||
enabled: True
|
||||
index: 5
|
||||
index: 5
|
||||
- config:
|
||||
description: subinterface configured by Ansible - 2
|
||||
enabled: False
|
||||
index: 2
|
||||
index: 2
|
||||
- config:
|
||||
description: configured by Ansible - 2
|
||||
enabled: False
|
||||
loopback-mode: False
|
||||
mtu: 2048
|
||||
name: loopback1111
|
||||
type: virt
|
||||
name: loopback1111
|
||||
subinterfaces:
|
||||
subinterface:
|
||||
- config:
|
||||
description: subinterface configured by Ansible - 3
|
||||
enabled: True
|
||||
index: 10
|
||||
index: 10
|
||||
- config:
|
||||
description: subinterface configured by Ansible - 4
|
||||
enabled: False
|
||||
index: 3
|
||||
index: 3
|
||||
|
||||
|
||||
- name: Find the description of loopback111, subinterface index 10
|
||||
debug:
|
||||
msg: |-
|
||||
{{ data.interfaces.interface[int_idx|int]
|
||||
.subinterfaces.subinterface[subint_idx|int]
|
||||
.config.description }}
|
||||
vars:
|
||||
# the values to search for
|
||||
int_name: loopback1111
|
||||
sub_index: 10
|
||||
# retrieve the index in each nested list
|
||||
int_idx: |
|
||||
{{ data.interfaces.interface|
|
||||
ansible.utils.index_of('eq', int_name, 'name') }}
|
||||
subint_idx: |
|
||||
{{ data.interfaces.interface[int_idx|int]
|
||||
.subinterfaces.subinterface|
|
||||
ansible.utils.index_of('eq', sub_index, 'index') }}
|
||||
|
||||
# TASK [Find the description of loopback111, subinterface index 10] ************
|
||||
# ok: [sw01] =>
|
||||
# msg: subinterface configured by Ansible - 3
|
||||
|
||||
|
||||
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
_raw:
|
||||
description:
|
||||
- One or more zero-based indicies of the matching list items
|
||||
- See C(wantlist) if a list is always required
|
||||
"""
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible_collections.ansible.utils.plugins.module_utils.common.index_of import (
|
||||
index_of,
|
||||
)
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
def run(self, terms, variables, **kwargs):
|
||||
kwargs["tests"] = self._templar.environment.tests
|
||||
if isinstance(terms, dict):
|
||||
terms.update(kwargs)
|
||||
res = index_of(**terms)
|
||||
else:
|
||||
res = index_of(*terms, **kwargs)
|
||||
if not isinstance(res, list):
|
||||
return [res]
|
||||
return res
|
|
@ -0,0 +1,214 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2020 Red Hat
|
||||
# GNU General Public License v3.0+
|
||||
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
|
||||
"""
|
||||
The index_of plugin common code
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
from ansible.module_utils.common.collections import is_sequence
|
||||
from ansible.module_utils.six import string_types, integer_types
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
# Note, this file can only be used on the control node
|
||||
# where ansible is installed
|
||||
# limit imports to filter and lookup plugins
|
||||
try:
|
||||
from ansible.errors import AnsibleError
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def _raise_error(msg):
|
||||
"""Raise an error message, prepend with filter name
|
||||
|
||||
:param msg: The message
|
||||
:type msg: str
|
||||
:raises: AnsibleError
|
||||
"""
|
||||
error = "Error when using plugin 'index_of': {msg}".format(msg=msg)
|
||||
raise AnsibleError(error)
|
||||
|
||||
|
||||
def _list_to_and_str(lyst):
|
||||
"""Convert a list to a command delimited string
|
||||
with the last entry being an and
|
||||
|
||||
:param lyst: The list to turn into a str
|
||||
:type lyst: list
|
||||
:return: The nicely formatted string
|
||||
:rtype: str
|
||||
"""
|
||||
res = "{most} and {last}".format(most=", ".join(lyst[:-1]), last=lyst[-1])
|
||||
return res
|
||||
|
||||
|
||||
def _to_well_known_type(obj):
|
||||
"""Convert an ansible internal type to a well-known type
|
||||
ie AnsibleUnicode => str
|
||||
|
||||
:param obj: the obj to convert
|
||||
:type obj: unknown
|
||||
"""
|
||||
return json.loads(json.dumps(obj))
|
||||
|
||||
|
||||
def _check_reqs(obj, wantlist):
|
||||
"""Check the args passed, ensure given a list
|
||||
|
||||
:param obj: The object passed to the filter plugin
|
||||
:type obj: unknown
|
||||
"""
|
||||
errors = []
|
||||
if not is_sequence(obj):
|
||||
msg = "a list is required, was passed a '{type}'.".format(
|
||||
type=type(_to_well_known_type(obj)).__name__
|
||||
)
|
||||
errors.append(msg)
|
||||
if not isinstance(wantlist, bool):
|
||||
msg = "'wantlist' is required to be a bool, was passed a '{type}'.".format(
|
||||
type=type(_to_well_known_type(wantlist)).__name__
|
||||
)
|
||||
errors.append(msg)
|
||||
if errors:
|
||||
_raise_error(" ".join(errors))
|
||||
|
||||
|
||||
def _run_test(entry, test, right, tests):
|
||||
"""Run a test
|
||||
|
||||
:param test: The test to run
|
||||
:type test: a lambda from the qual_map
|
||||
:param entry: The x for the lambda
|
||||
:type entry: str int or bool
|
||||
:param right: The y for the lamba
|
||||
:type right: str int bool or list
|
||||
:return: If the test passed
|
||||
:rtype: book
|
||||
"""
|
||||
msg = (
|
||||
"Error encountered when testing value "
|
||||
"'{entry}' (type={entry_type}) against "
|
||||
"'{right}' (type={right_type}) with '{test}'. "
|
||||
).format(
|
||||
entry=entry,
|
||||
entry_type=type(_to_well_known_type(entry)).__name__,
|
||||
right=right,
|
||||
right_type=type(_to_well_known_type(entry)).__name__,
|
||||
test=test,
|
||||
)
|
||||
|
||||
if test.startswith("!"):
|
||||
invert = True
|
||||
test = test.lstrip("!")
|
||||
if test == "=":
|
||||
test = "=="
|
||||
elif test.startswith("not "):
|
||||
invert = True
|
||||
test = test.lstrip("not ")
|
||||
else:
|
||||
invert = False
|
||||
|
||||
if not isinstance(right, list) and test == "in":
|
||||
right = [right]
|
||||
|
||||
j2_test = tests.get(test)
|
||||
if not j2_test:
|
||||
msg = "{msg} Error was: the test '{test}' was not found.".format(
|
||||
msg=msg, test=test
|
||||
)
|
||||
_raise_error(msg)
|
||||
else:
|
||||
try:
|
||||
if right is None:
|
||||
result = j2_test(entry)
|
||||
else:
|
||||
result = j2_test(entry, right)
|
||||
except Exception as exc:
|
||||
msg = "{msg} Error was: {error}".format(
|
||||
msg=msg, error=to_native(exc)
|
||||
)
|
||||
_raise_error(msg)
|
||||
|
||||
if invert:
|
||||
result = not result
|
||||
return result
|
||||
|
||||
|
||||
def index_of(
|
||||
data,
|
||||
test,
|
||||
value=None,
|
||||
key=None,
|
||||
wantlist=False,
|
||||
fail_on_missing=False,
|
||||
tests=None,
|
||||
):
|
||||
"""Find the index or indices of entries in list of objects"
|
||||
|
||||
:param data: The data passed in (data|index_of(...))
|
||||
:type data: unknown
|
||||
:param test: the test to use
|
||||
:type test: jinj2 test
|
||||
:param value: The value to use for the test
|
||||
:type value: unknown
|
||||
:param key: The key to use when a list of dicts is passed
|
||||
:type key: valid key type
|
||||
:param want_list: always return a list, even if 1 index
|
||||
:type want_list: bool
|
||||
:param fail_on_missing: Should we fail if key not found?
|
||||
:type fail_on_missing: bool
|
||||
:param tests: The jinja tests from the current environment
|
||||
:type tests: ansible.template.JinjaPluginIntercept
|
||||
"""
|
||||
_check_reqs(data, wantlist)
|
||||
res = list()
|
||||
if key is None:
|
||||
for idx, entry in enumerate(data):
|
||||
result = _run_test(entry, test, value, tests)
|
||||
if result:
|
||||
res.append(idx)
|
||||
|
||||
elif isinstance(key, (string_types, integer_types, bool)):
|
||||
|
||||
if not all(isinstance(entry, dict) for entry in data):
|
||||
all_tipes = [
|
||||
type(_to_well_known_type(entry)).__name__ for entry in data
|
||||
]
|
||||
msg = (
|
||||
"When a key name is provided, all list entries are required to "
|
||||
"be dictionaries, got {str_tipes}"
|
||||
).format(str_tipes=_list_to_and_str(all_tipes))
|
||||
_raise_error(msg)
|
||||
errors = []
|
||||
for idx, dyct in enumerate(data):
|
||||
if key in dyct:
|
||||
entry = dyct.get(key)
|
||||
result = _run_test(entry, test, value, tests)
|
||||
if result:
|
||||
res.append(idx)
|
||||
elif fail_on_missing:
|
||||
msg = (
|
||||
"'{key}' was not found in '{dyct}' at [{index}]"
|
||||
).format(key=key, dyct=dyct, index=idx)
|
||||
errors.append(msg)
|
||||
if errors:
|
||||
_raise_error(
|
||||
("{errors}. fail_on_missing={fom}").format(
|
||||
errors=_list_to_and_str(errors), fom=str(fail_on_missing)
|
||||
)
|
||||
)
|
||||
else:
|
||||
msg = "Unknown key type, key ({key}) was a {type}. ".format(
|
||||
key=key, type=type(_to_well_known_type(key)).__name__
|
||||
)
|
||||
_raise_error(msg)
|
||||
if len(res) == 1 and not wantlist:
|
||||
return res[0]
|
||||
return res
|
|
@ -0,0 +1,254 @@
|
|||
# These are the examples for the lookup plugin
|
||||
# here for future use if needed
|
||||
# Note: the cisco example has been commented out
|
||||
# to eliminate a cross collection dependancy
|
||||
|
||||
- set_fact:
|
||||
data:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
|
||||
- name: Find the index of 2, lookup or filter
|
||||
set_fact:
|
||||
as_lookup: "{{ lookup('ansible.utils.index_of', data, 'eq', 2) }}"
|
||||
as_filter: "{{ data|ansible.utils.index_of('eq', 2) }}"
|
||||
|
||||
# TASK [Find the index of 2, lookup or filter] *******************************
|
||||
# ok: [sw01] => changed=false
|
||||
# ansible_facts:
|
||||
# as_filter: '1'
|
||||
# as_lookup: '1'
|
||||
|
||||
- name: Find the index of 2, lookup or filter, ensure list is returned
|
||||
set_fact:
|
||||
as_query: "{{ query('ansible.utils.index_of', data, 'eq', 2) }}"
|
||||
as_lookup: "{{ lookup('ansible.utils.index_of', data, 'eq', 2, wantlist=True) }}"
|
||||
as_filter: "{{ data|ansible.utils.index_of('eq', 2, wantlist=True) }}"
|
||||
|
||||
# TASK [Find the index of 2, lookup or filter, ensure list is returned] ******
|
||||
# ok: [sw01] => changed=false
|
||||
# ansible_facts:
|
||||
# as_filter:
|
||||
# - 1
|
||||
# as_lookup:
|
||||
# - 1
|
||||
# as_query:
|
||||
# - 1
|
||||
|
||||
- name: Find the index of 3 using the long format
|
||||
set_fact:
|
||||
as_query: "{{ query('ansible.utils.index_of', data=data, test='eq', value=value) }}"
|
||||
as_lookup: "{{ lookup('ansible.utils.index_of', data=data, test='eq',value =value, wantlist=True) }}"
|
||||
as_filter: "{{ data|ansible.utils.index_of(test='eq', value=value, wantlist=True) }}"
|
||||
vars:
|
||||
value: 3
|
||||
|
||||
# TASK [Find the index of 3 using the long format] ***************************
|
||||
# ok: [sw01] => changed=false
|
||||
# ansible_facts:
|
||||
# as_filter:
|
||||
# - 2
|
||||
# as_lookup:
|
||||
# - 2
|
||||
# as_query:
|
||||
# - 2
|
||||
|
||||
- name: Find numbers greater than 1, using loop
|
||||
debug:
|
||||
msg: "{{ data[item] }} is {{ test }} than {{ value }}"
|
||||
loop: "{{ data|ansible.utils.index_of(test, value) }}"
|
||||
vars:
|
||||
test: '>'
|
||||
value: 1
|
||||
|
||||
# TASK [Find numbers great than 1, using loop] *******************************
|
||||
# ok: [sw01] => (item=1) =>
|
||||
# msg: 2 is > than 1
|
||||
# ok: [sw01] => (item=2) =>
|
||||
# msg: 3 is > than 1
|
||||
|
||||
- name: Find numbers greater than 1, using with
|
||||
debug:
|
||||
msg: "{{ data[item] }} is {{ params.test }} than {{ params.value }}"
|
||||
with_ansible.utils.index_of: "{{ params }}"
|
||||
vars:
|
||||
params:
|
||||
data: "{{ data }}"
|
||||
test: '>'
|
||||
value: 1
|
||||
|
||||
|
||||
- set_fact:
|
||||
data:
|
||||
- name: sw01.example.lan
|
||||
type: switch
|
||||
- name: rtr01.example.lan
|
||||
type: router
|
||||
- name: fw01.example.corp
|
||||
type: firewall
|
||||
- name: fw02.example.corp
|
||||
type: firewall
|
||||
|
||||
- name: Find the index of all firewalls using the type key
|
||||
set_fact:
|
||||
as_query: "{{ query('ansible.utils.index_of', data, 'eq', 'firewall', 'type') }}"
|
||||
as_lookup: "{{ lookup('ansible.utils.index_of', data, 'eq', 'firewall', 'type') }}"
|
||||
as_filter: "{{ data|ansible.utils.index_of('eq', 'firewall', 'type') }}"
|
||||
|
||||
# TASK [Find the index of all firewalls using the type key] ******************
|
||||
# ok: [sw01] => changed=false
|
||||
# ansible_facts:
|
||||
# as_filter:
|
||||
# - 2
|
||||
# - 3
|
||||
# as_lookup:
|
||||
# - 2
|
||||
# - 3
|
||||
# as_query:
|
||||
# - 2
|
||||
# - 3
|
||||
|
||||
- name: Find the index of all firewalls, use in a loop, as a filter
|
||||
debug:
|
||||
msg: "The type of {{ device_type }} at index {{ item }} has name {{ data[item].name }}."
|
||||
loop: "{{ data|ansible.utils.index_of('eq', device_type, 'type') }}"
|
||||
vars:
|
||||
device_type: firewall
|
||||
|
||||
# TASK [Find the index of all firewalls, use in a loop] **********************
|
||||
# ok: [sw01] => (item=2) =>
|
||||
# msg: The type of firewall at index 2 has name fw01.
|
||||
# ok: [sw01] => (item=3) =>
|
||||
# msg: The type of firewall at index 3 has name fw02.
|
||||
|
||||
|
||||
- name: Find the index of all devices with a .corp name, as a lookup
|
||||
debug:
|
||||
msg: "The device named {{ data[item].name }} is a {{ data[item].type }}"
|
||||
loop: "{{ lookup('ansible.utils.index_of', data, 'regex', regex, 'name') }}"
|
||||
vars:
|
||||
regex: '\.corp$' # ends with .corp
|
||||
|
||||
# TASK [Find the index of all devices with a .corp name, as a lookup] **********
|
||||
# ok: [sw01] => (item=2) =>
|
||||
# msg: The device named fw01.example.corp is a firewall
|
||||
# ok: [sw01] => (item=3) =>
|
||||
# msg: The device named fw02.example.corp is a firewall
|
||||
|
||||
# - name: Retrieve the current L3 interface configuration
|
||||
# cisco.nxos.nxos_l3_interfaces:
|
||||
# state: gathered
|
||||
# register: current_l3
|
||||
|
||||
# TASK [Retrieve the current L3 interface configuration] *********************
|
||||
# ok: [sw01] => changed=false
|
||||
# gathered:
|
||||
# - name: Ethernet1/1
|
||||
# - name: Ethernet1/2
|
||||
# <...>
|
||||
# - name: Ethernet1/128
|
||||
# - ipv4:
|
||||
# - address: 192.168.101.14/24
|
||||
# name: mgmt0
|
||||
|
||||
# - name: Find the index of the interface and address with a 192.168.101.xx ip address
|
||||
# set_fact:
|
||||
# found: "{{ found + entry }}"
|
||||
# with_indexed_items: "{{ current_l3.gathered }}"
|
||||
# vars:
|
||||
# found: []
|
||||
# ip: '192.168.101.'
|
||||
# address: "{{ item.1.ipv4|d([])|ansible.utils.index_of('search', ip, 'address', wantlist=True) }}"
|
||||
# entry:
|
||||
# - interface_idx: "{{ item.0 }}"
|
||||
# address_idxs: "{{ address }}"
|
||||
# when: address
|
||||
|
||||
# TASK [debug] ***************************************************************
|
||||
# ok: [sw01] =>
|
||||
# found:
|
||||
# - address_idxs:
|
||||
# - 0
|
||||
# interface_idx: '128'
|
||||
|
||||
# - name: Show all interfaces and their address
|
||||
# debug:
|
||||
# msg: "{{ interface.name }} has ip {{ address }}"
|
||||
# loop: "{{ found|subelements('address_idxs') }}"
|
||||
# vars:
|
||||
# interface: "{{ current_l3.gathered[item.0.interface_idx|int] }}"
|
||||
# address: "{{ interface.ipv4[item.1].address }}"
|
||||
|
||||
# TASK [debug] ***************************************************************
|
||||
# ok: [sw01] => (item=[{'interface_idx': '128', 'address_idx': [0]}, 0]) =>
|
||||
# msg: mgmt0 has ip 192.168.101.14/24
|
||||
|
||||
- set_fact:
|
||||
data:
|
||||
interfaces:
|
||||
interface:
|
||||
- config:
|
||||
description: configured by Ansible - 1
|
||||
enabled: True
|
||||
loopback-mode: False
|
||||
mtu: 1024
|
||||
name: loopback0000
|
||||
type: eth
|
||||
name: loopback0000
|
||||
subinterfaces:
|
||||
subinterface:
|
||||
- config:
|
||||
description: subinterface configured by Ansible - 1
|
||||
enabled: True
|
||||
index: 5
|
||||
index: 5
|
||||
- config:
|
||||
description: subinterface configured by Ansible - 2
|
||||
enabled: False
|
||||
index: 2
|
||||
index: 2
|
||||
- config:
|
||||
description: configured by Ansible - 2
|
||||
enabled: False
|
||||
loopback-mode: False
|
||||
mtu: 2048
|
||||
name: loopback1111
|
||||
type: virt
|
||||
name: loopback1111
|
||||
subinterfaces:
|
||||
subinterface:
|
||||
- config:
|
||||
description: subinterface configured by Ansible - 3
|
||||
enabled: True
|
||||
index: 10
|
||||
index: 10
|
||||
- config:
|
||||
description: subinterface configured by Ansible - 4
|
||||
enabled: False
|
||||
index: 3
|
||||
index: 3
|
||||
|
||||
|
||||
- name: Find the description of loopback111, subinterface index 10
|
||||
debug:
|
||||
msg: |-
|
||||
{{ data.interfaces.interface[int_idx|int]
|
||||
.subinterfaces.subinterface[subint_idx|int]
|
||||
.config.description }}
|
||||
vars:
|
||||
# the values to search for
|
||||
int_name: loopback1111
|
||||
sub_index: 10
|
||||
# retrieve the index in each nested list
|
||||
int_idx: |
|
||||
{{ data.interfaces.interface|
|
||||
ansible.utils.index_of('eq', int_name, 'name') }}
|
||||
subint_idx: |
|
||||
{{ data.interfaces.interface[int_idx|int]
|
||||
.subinterfaces.subinterface|
|
||||
ansible.utils.index_of('eq', sub_index, 'index') }}
|
||||
|
||||
# TASK [Find the description of loopback111, subinterface index 10] ************
|
||||
# ok: [sw01] =>
|
||||
# msg: subinterface configured by Ansible - 3
|
|
@ -0,0 +1,2 @@
|
|||
- include: simple.yaml
|
||||
- include: examples.yaml
|
|
@ -0,0 +1,107 @@
|
|||
- set_fact:
|
||||
complex:
|
||||
a:
|
||||
- True
|
||||
- True
|
||||
- False
|
||||
- 5
|
||||
|
||||
b:
|
||||
- b1: 1
|
||||
b2: 2
|
||||
- b1: 3
|
||||
b2: 4
|
||||
c:
|
||||
c1:
|
||||
- a
|
||||
- b
|
||||
- c
|
||||
d:
|
||||
- Abcd
|
||||
- abcd
|
||||
- B
|
||||
- b
|
||||
|
||||
- name: Some basic tests
|
||||
assert:
|
||||
that: "{{ item.test == item.result }}"
|
||||
loop:
|
||||
- test: "{{ complex.a|ansible.utils.index_of('eq', True) }}"
|
||||
result: [0, 1]
|
||||
- test: "{{ lookup('ansible.utils.index_of', complex.a, 'eq', True) }}"
|
||||
result: [0, 1]
|
||||
- test: "{{ complex.a|ansible.utils.index_of('in', [True, False]) }}"
|
||||
result: [0, 1, 2]
|
||||
- test: "{{ lookup('ansible.utils.index_of', complex.a, 'in', [True, False]) }}"
|
||||
result: [0, 1, 2]
|
||||
# These are commented out due to jinja < 2.11 w/ 2.9, 'integer' not avaialable
|
||||
# can be enabled at a later date
|
||||
# - test: "{{ complex.a|ansible.utils.index_of('integer') }}"
|
||||
# result: "3"
|
||||
# - test: "{{ lookup('ansible.utils.index_of', complex.a, 'integer') }}"
|
||||
# result: "3"
|
||||
|
||||
- test: "{{ complex.b|ansible.utils.index_of('==', 1, 'b1') }}"
|
||||
result: "0"
|
||||
- test: "{{ lookup('ansible.utils.index_of', complex.b, '==', 1, 'b1') }}"
|
||||
result: "0"
|
||||
|
||||
- test: "{{ complex.c.c1|ansible.utils.index_of('!=', 'c') }}"
|
||||
result: [0, 1]
|
||||
- test: "{{ lookup('ansible.utils.index_of', complex.c.c1, '!=', 'c') }}"
|
||||
result: [0, 1]
|
||||
|
||||
- test: "{{ complex.d|ansible.utils.index_of('match', '.*d$') }}"
|
||||
result: [0, 1]
|
||||
- test: "{{ lookup('ansible.utils.index_of', complex.d, 'match', '.*d$') }}"
|
||||
result: [0, 1]
|
||||
|
||||
|
||||
- set_fact:
|
||||
complex:
|
||||
a:
|
||||
b:
|
||||
c:
|
||||
d:
|
||||
- e0: 0
|
||||
e1: ansible
|
||||
e2: True
|
||||
- e0: 1
|
||||
e1: redhat
|
||||
|
||||
- name: Find index in list of dictionaries
|
||||
assert:
|
||||
that: "{{ item.test == item.result }}"
|
||||
loop:
|
||||
- test: "{{ complex.a.b.c.d|ansible.utils.index_of('eq', 'ansible', 'e1') }}"
|
||||
result: "0"
|
||||
- test: "{{ lookup('ansible.utils.index_of', complex.a.b.c.d, 'eq', 'ansible', 'e1') }}"
|
||||
result: "0"
|
||||
- test: "{{ complex.a.b.c.d|ansible.utils.index_of('eq', 'ansible', 'e1', wantlist=True) }}"
|
||||
result: [0]
|
||||
- test: "{{ lookup('ansible.utils.index_of', complex.a.b.c.d, 'eq', 'ansible', 'e1', wantlist=True) }}"
|
||||
result: [0]
|
||||
|
||||
- name: Test a missing key in the list of dictionaries
|
||||
assert:
|
||||
that: "{{ item.test == item.result }}"
|
||||
loop:
|
||||
- test: "{{ complex.a.b.c.d|ansible.utils.index_of('eq', True, 'e2') }}"
|
||||
result: "0"
|
||||
- test: "{{ lookup('ansible.utils.index_of', complex.a.b.c.d, 'eq', True, 'e2') }}"
|
||||
result: "0"
|
||||
|
||||
- name: Test a missing key in the list of dictionaries, fail on missing
|
||||
assert:
|
||||
that: "{{ item.test == item.result }}"
|
||||
loop:
|
||||
- test: "{{ complex.a.b.c.d|ansible.utils.index_of('eq', True, 'e2', fail_on_missing=True) }}"
|
||||
result: "0"
|
||||
- test: "{{ lookup('ansible.utils.index_of', complex.a.b.c.d, 'eq', True, 'e2', fail_on_missing=True) }}"
|
||||
result: "0"
|
||||
ignore_errors: True
|
||||
register: result
|
||||
|
||||
- name: Ensure the previous test failed
|
||||
assert:
|
||||
that: "{{ result.failed and 'not found in' in result.msg }}"
|
|
@ -1 +1,2 @@
|
|||
plugins/module_utils/common/path.py pylint:ansible-bad-module-import # file's use is limited to filter and lookups on control node
|
||||
plugins/module_utils/common/index_of.py pylint:ansible-bad-module-import # file's use is limited to filter and lookups on control node
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
plugins/module_utils/common/path.py pylint:ansible-bad-module-import # file's use is limited to filter and lookups on control node
|
||||
plugins/module_utils/common/index_of.py pylint:ansible-bad-module-import # file's use is limited to filter and lookups on control node
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
plugins/module_utils/common/path.py pylint:ansible-bad-module-import # file's use is limited to filter and lookups on control node
|
||||
plugins/module_utils/common/index_of.py pylint:ansible-bad-module-import # file's use is limited to filter and lookups on control node
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2020 Red Hat
|
||||
# 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
|
||||
|
||||
|
||||
import unittest
|
||||
from ansible_collections.ansible.utils.plugins.module_utils.common.index_of import (
|
||||
index_of,
|
||||
)
|
||||
from ansible.template import Templar
|
||||
|
||||
|
||||
class TestIndexOfFilter(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._tests = Templar(loader=None).environment.tests
|
||||
|
||||
def test_fail_no_qualfier(self):
|
||||
obj, test, value = [1, 2], "@@", 1
|
||||
with self.assertRaises(Exception) as exc:
|
||||
index_of(obj, test, value, tests=self._tests)
|
||||
self.assertIn("the test '@@' was not found", str(exc.exception))
|
||||
obj, test, value, key = [{"a": 1}], "@@", 1, "a"
|
||||
with self.assertRaises(Exception) as exc:
|
||||
index_of(obj, test, value, key, tests=self._tests)
|
||||
self.assertIn("the test '@@' was not found", str(exc.exception))
|
||||
|
||||
def test_fail_not_a_list(self):
|
||||
obj, test, value = True, "==", 1
|
||||
with self.assertRaises(Exception) as exc:
|
||||
index_of(obj, test, value, tests=self._tests)
|
||||
self.assertIn(
|
||||
"a list is required, was passed a 'bool'", str(exc.exception)
|
||||
)
|
||||
|
||||
def test_fail_wantlist_not_a_bool(self):
|
||||
obj, test, value = [1, 2], "==", 1
|
||||
with self.assertRaises(Exception) as exc:
|
||||
index_of(obj, test, value, wantlist=42, tests=self._tests)
|
||||
self.assertIn(
|
||||
"'wantlist' is required to be a bool, was passed a 'int'",
|
||||
str(exc.exception),
|
||||
)
|
||||
|
||||
def test_fail_mixed_list(self):
|
||||
obj, test, value, key = [{"a": "b"}, True, 1, "a"], "==", "b", "a"
|
||||
with self.assertRaises(Exception) as exc:
|
||||
index_of(obj, test, value, key, tests=self._tests)
|
||||
self.assertIn("required to be dictionaries", str(exc.exception))
|
||||
|
||||
def test_fail_key_not_valid(self):
|
||||
obj, test, value, key = [{"a": "b"}], "==", "b", [1, 2]
|
||||
with self.assertRaises(Exception) as exc:
|
||||
index_of(obj, test, value, key, tests=self._tests)
|
||||
self.assertIn("Unknown key type", str(exc.exception))
|
||||
|
||||
def test_fail_on_missing(self):
|
||||
obj, test, value, key = [{"a": True}, {"c": False}], "==", True, "a"
|
||||
with self.assertRaises(Exception) as exc:
|
||||
index_of(
|
||||
obj, test, value, key, fail_on_missing=True, tests=self._tests
|
||||
)
|
||||
self.assertIn("'a' was not found", str(exc.exception))
|
||||
|
||||
def test_just_test(self):
|
||||
"""Limit to jinja < 2.11 tests"""
|
||||
objs = [
|
||||
# ([True], "true", 0),
|
||||
# ([False], "not false", []),
|
||||
# ([False, 5], "boolean", 0),
|
||||
# ([0, False], "false", 1),
|
||||
([3, 4], "even", 1),
|
||||
([3, 3], "even", []),
|
||||
([3, 3, 3, 4], "odd", [0, 1, 2]),
|
||||
# ([3.3, 3.4], "float", [0, 1]),
|
||||
]
|
||||
for entry in objs:
|
||||
obj, test, answer = entry
|
||||
result = index_of(obj, test, tests=self._tests)
|
||||
expected = answer
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_simple_lists(self):
|
||||
objs = [
|
||||
([1, 2, 3], "==", 2, 1),
|
||||
(["a", "b", "c"], "eq", "c", 2),
|
||||
([True, False, 0, 1], "equalto", False, [1, 2]),
|
||||
([True, False, "0", "1"], "==", False, 1),
|
||||
([True, False, "", "1"], "==", False, 1),
|
||||
([True, False, "", "1"], "in", False, 1),
|
||||
([True, False, "", "1", "a"], "in", [False, "1"], [1, 3]),
|
||||
([1, 2, 3, "a", "b", "c"], "!=", "c", [0, 1, 2, 3, 4]),
|
||||
([1, 2, 3], "!<", 3, 2),
|
||||
]
|
||||
for entry in objs:
|
||||
obj, test, value, answer = entry
|
||||
result = index_of(obj, test, value, tests=self._tests)
|
||||
expected = answer
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
def test_simple_dict(self):
|
||||
objs = [
|
||||
([{"a": 1}], "==", 1, "a", 0),
|
||||
([{"a": 1}], "==", 1, "b", []),
|
||||
([{"a": 1}], "==", 2, "a", []),
|
||||
(
|
||||
[{"a": 1}, {"a": 1}, {"a": 1}, {"a": 2}],
|
||||
"==",
|
||||
1,
|
||||
"a",
|
||||
[0, 1, 2],
|
||||
),
|
||||
(
|
||||
[{"a": "abc"}, {"a": "def"}, {"a": "ghi"}, {"a": "jkl"}],
|
||||
"ansible.builtin.match",
|
||||
"^a",
|
||||
"a",
|
||||
0,
|
||||
),
|
||||
(
|
||||
[{"a": "abc"}, {"a": "def"}, {"a": "ghi"}, {"a": "jkl"}],
|
||||
"ansible.builtin.search",
|
||||
"e",
|
||||
"a",
|
||||
1,
|
||||
),
|
||||
]
|
||||
for entry in objs:
|
||||
obj, test, value, key, answer = entry
|
||||
result = index_of(obj, test, value, key, tests=self._tests)
|
||||
self.assertEqual(result, answer)
|
Loading…
Reference in New Issue