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
Bradley A. Thornton 2020-10-15 05:52:14 -07:00 committed by GitHub
parent 309ccb5563
commit 197b9d93b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1642 additions and 14 deletions

View File

@ -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-->

View File

@ -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.

View File

@ -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}

373
plugins/lookup/index_of.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
- include: simple.yaml
- include: examples.yaml

View File

@ -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 }}"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File