From 9d3a6cad0a184452879c80284c1227d24caf2805 Mon Sep 17 00:00:00 2001 From: cidrblock Date: Fri, 9 Oct 2020 10:07:26 -0700 Subject: [PATCH] get_path and to_paths --- README.md | 80 ++++-- docs/ansible.utils.get_path_lookup.rst | 270 ++++++++++++++++++ docs/ansible.utils.to_paths_lookup.rst | 253 ++++++++++++++++ galaxy.yml | 34 +-- meta/runtime.yml | 2 +- plugins/filter/paths.py | 24 +- plugins/lookup/get_path.py | 174 +++++++++++ plugins/lookup/to_paths.py | 157 ++++++++++ .../{generate_paths.py => path_utils.py} | 41 ++- tests/sanity/ignore-2.10.txt | 1 + tests/sanity/ignore-2.9.txt | 1 + 11 files changed, 974 insertions(+), 63 deletions(-) create mode 100644 docs/ansible.utils.get_path_lookup.rst create mode 100644 docs/ansible.utils.to_paths_lookup.rst create mode 100644 plugins/lookup/get_path.py create mode 100644 plugins/lookup/to_paths.py rename plugins/module_utils/{generate_paths.py => path_utils.py} (50%) create mode 100644 tests/sanity/ignore-2.10.txt create mode 100644 tests/sanity/ignore-2.9.txt diff --git a/README.md b/README.md index 0ddb655..0ca0592 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,76 @@ -# collection_template -You can build a new repository for an Ansible Collection using this template by following [Creating a repository from a template](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template). This README.md contains recommended headings for your collection README.md, with comments describing what each section should contain. Once you have created your collection repository, delete this paragraph and the title above it from your README.md. -# Foo Collection - -[![CI](https://github.com/ansible-collections/REPONAMEHERE/workflows/CI/badge.svg?event=push)](https://github.com/ansible-collections/REPONAMEHERE/actions) [![Codecov](https://img.shields.io/codecov/c/github/ansible-collections/REPONAMEHERE)](https://codecov.io/gh/ansible-collections/REPONAMEHERE) - +# Ansible Utilities Collection +[![CI](https://zuul-ci.org/gated.svg)](https://dashboard.zuul.ansible.com/t/ansible/builds?project=ansible-collections%2Fansible.utils) -## Tested with Ansible +The Ansible ``ansible.utils`` collection includes FIXME - + +## Ansible version compatibility -## External requirements +This collection has been tested against following Ansible versions: **>=2.9.10,<2.11**. - - -### Supported connections - +Plugins and modules within a collection may be tested with only specific Ansible versions. +A collection may contain metadata that identifies these versions. +PEP440 is the schema used to describe the versions of Ansible. + ## Included content - + +### 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) +### 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.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 + + + +## Installing this collection + +You can install the ``ansible.utils`` collection with the Ansible Galaxy CLI: + + ansible-galaxy collection install ansible.utils + +You can also include it in a `requirements.yml` file and install it with `ansible-galaxy collection install -r requirements.yml`, using the format: + +```yaml +--- +collections: + - name: ansible.utils +``` ## Using this collection - +The most common use case for this collection is FIXME -See [Ansible Using collections](https://docs.ansible.com/ansible/latest/user_guide/collections_using.html) for more details. + +**NOTE**: For Ansible 2.9, you may not see deprecation warnings when you run your playbooks with this collection. Use this documentation to track when a module is deprecated. + +### See Also: + +* [Ansible Using collections](https://docs.ansible.com/ansible/latest/user_guide/collections_using.html) for more details. ## Contributing to this collection - +We welcome community contributions to this collection. If you find problems, please open an issue or create a PR against the [ansible.utils collection repository](https://github.com/ansible-collections/ansible.utils). See [Contributing to Ansible-maintained collections](https://docs.ansible.com/ansible/devel/community/contributing_maintained_collections.html#contributing-maintained-collections) for complete details. + +See the [Ansible Community Guide](https://docs.ansible.com/ansible/latest/community/index.html) for details on contributing to Ansible. + +### Code of Conduct +This collection follows the Ansible project's +[Code of Conduct](https://docs.ansible.com/ansible/devel/community/code_of_conduct.html). +Please read and familiarize yourself with this document. ## Release notes - -See the [changelog](https://github.com/ansible-collections/REPONAMEHERE/tree/main/CHANGELOG.rst). + +Release notes are available [here](https://github.com/ansible-collections/ansible.utils/blob/main/changelogs/CHANGELOG.rst) ## Roadmap @@ -43,20 +78,13 @@ See the [changelog](https://github.com/ansible-collections/REPONAMEHERE/tree/mai ## More information - - - [Ansible Collection overview](https://github.com/ansible-collections/overview) - [Ansible User guide](https://docs.ansible.com/ansible/latest/user_guide/index.html) - [Ansible Developer guide](https://docs.ansible.com/ansible/latest/dev_guide/index.html) -- [Ansible Collections Checklist](https://github.com/ansible-collections/overview/blob/master/collection_requirements.rst) - [Ansible Community code of conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) -- [The Bullhorn (the Ansible Contributor newsletter)](https://us19.campaign-archive.com/home/?u=56d874e027110e35dea0e03c1&id=d6635f5420) -- [Changes impacting Contributors](https://github.com/ansible-collections/overview/issues/45) ## Licensing - - GNU General Public License v3.0 or later. See [LICENSE](https://www.gnu.org/licenses/gpl-3.0.txt) to see the full text. diff --git a/docs/ansible.utils.get_path_lookup.rst b/docs/ansible.utils.get_path_lookup.rst new file mode 100644 index 0000000..649e046 --- /dev/null +++ b/docs/ansible.utils.get_path_lookup.rst @@ -0,0 +1,270 @@ +.. _ansible.utils.get_path_lookup: + + +********************** +ansible.utils.get_path +********************** + +**Retrieve the value in a variable using a path** + + +Version added: 1.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Use a ``path`` to retreive a nested value from a ``var`` +- ``get_path`` is also available as a ``filter_plugin`` for convenience + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ _terms + +
+ - + / required +
+
+ + +
The values below provided in the order var, path, wantlist=.
+
+
+ path + +
+ string + / required +
+
+ + +
The path in the var to retrieve the value of. The path needs to a be a valid jinja path
+
+
+ var + +
+ raw + / required +
+
+ + +
The variable from which the value should be extraced
+
+
+ wantlist + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes
  • +
+
+ +
If set to True, the return value will always be a list This can also be accomplished using query or q instead of lookup https://docs.ansible.com/ansible/latest/plugins/lookup.html
+
+
+ + + + +Examples +-------- + +.. code-block:: yaml+jinja + + - ansible.builtin.set_fact: + a: + b: + c: + d: + - 0 + - 1 + e: + - True + - False + + - name: Retrieve a value deep inside a using a path + ansible.builtin.set_fact: + as_lookup: "{{ lookup('ansible.utils.get_path', a, path) }}" + as_filter: "{{ a|ansible.utils.get_path(path) }}" + vars: + path: b.c.d[0] + + # TASK [ansible.builtin.set_fact] ************************************* + # ok: [nxos101] => changed=false + # ansible_facts: + # as_filter: '0' + # as_lookup: '0' + + + #### Working with hostvars + + - name: Retrieve a value deep inside all of the host's vars + ansible.builtin.set_fact: + as_lookup: "{{ lookup('ansible.utils.get_path', look_in, look_for) }}" + as_filter: "{{ look_in|ansible.utils.get_path(look_for) }}" + vars: + look_in: "{{ hostvars[inventory_hostname] }}" + look_for: a.b.c.d[0] + + # TASK [Retrieve a value deep inside all of the host's vars] ********** + # ok: [nxos101] => changed=false + # ansible_facts: + # as_filter: '0' + # as_lookup: '0' + + + #### Used alongside ansible.utils.to_paths + + - name: Get the paths for the object + ansible.builtin.set_fact: + paths: "{{ a|ansible.utils.to_paths(prepend='a') }}" + + - name: Retrieve the value of each path from vars + ansible.builtin.debug: + msg: "The value of path {{ path }} in vars is {{ value }}" + loop: "{{ paths.keys()|list }}" + loop_control: + label: "{{ item }}" + vars: + path: "{{ item }}" + value: "{{ vars|ansible.utils.get_path(item) }}" + + # TASK [Get the paths for the object] ********************************* + # ok: [nxos101] => changed=false + # ansible_facts: + # paths: + # a.b.c.d[0]: 0 + # a.b.c.d[1]: 1 + # a.b.c.e[0]: true + # a.b.c.e[1]: false + + # TASK [Retrieve the value of each path from vars] ******************** + # ok: [nxos101] => (item=a.b.c.d[0]) => + # msg: The value of path a.b.c.d[0] in vars is 0 + # ok: [nxos101] => (item=a.b.c.d[1]) => + # msg: The value of path a.b.c.d[1] in vars is 1 + # ok: [nxos101] => (item=a.b.c.e[0]) => + # msg: The value of path a.b.c.e[0] in vars is True + # ok: [nxos101] => (item=a.b.c.e[1]) => + # msg: The value of path a.b.c.e[1] in vars is False + + + #### Working with complex structures + + - name: Retrieve the current interface config + cisco.nxos.nxos_interfaces: + state: gathered + register: interfaces + + - name: Get the description of several interfaces + ansible.builtin.debug: + msg: "{{ rekeyed|ansible.utils.get_path(item) }}" + vars: + rekeyed: + by_name: "{{ interfaces.gathered|ansible.builtin.rekey_on_member('name') }}" + loop: + - by_name['Ethernet1/1'].description + - by_name['Ethernet1/2'].description + + # TASK [Get the description of several interfaces] ******************** + # ok: [nxos101] => (item=by_name['Ethernet1/1'].description) => + # msg: Configured by Ansible + # ok: [nxos101] => (item=by_name['Ethernet1/2'].description) => + # msg: Configured by Ansible Network + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this lookup: + +.. raw:: html + + + + + + + + + + + + +
KeyReturnedDescription
+
+ _raw + +
+ - +
+
+
One or more zero-based indicies of the matching list items
+
See wantlist if a list is always required
+
+
+

+ + +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. diff --git a/docs/ansible.utils.to_paths_lookup.rst b/docs/ansible.utils.to_paths_lookup.rst new file mode 100644 index 0000000..329769e --- /dev/null +++ b/docs/ansible.utils.to_paths_lookup.rst @@ -0,0 +1,253 @@ +.. _ansible.utils.to_paths_lookup: + + +********************** +ansible.utils.to_paths +********************** + +**Flatten a complex object into a dictionary of paths and values** + + +Version added: 1.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Flatten a complex object into a dictionary of paths and values. +- Paths are dot delimited whenever possible +- Brakets are used for list indicies and keys that contain special characters +- ``to_paths`` is also available as a filter plugin + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsConfigurationComments
+
+ _terms + +
+ - + / required +
+
+ + +
The values below provided in the order var, prepend=, wantlist=.
+
+
+ prepend + +
+ string +
+
+ + +
Prepend each path entry. Useful to add the initial var name.
+
+
+ var + +
+ raw + / required +
+
+ + +
The value of var will be will be used.
+
+
+ wantlist + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes
  • +
+
+ +
If set to True, the return value will always be a list. This can also be accomplished using query or q instead of lookup. https://docs.ansible.com/ansible/latest/plugins/lookup.html
+
+
+ + + + +Examples +-------- + +.. code-block:: yaml+jinja + + #### Simple examples + + - ansible.builtin.set_fact: + a: + b: + c: + d: + - 0 + - 1 + e: + - True + - False + + - ansible.builtin.set_fact: + as_lookup: "{{ lookup('ansible.utils.to_paths', a) }}" + as_filter: "{{ a|ansible.utils.to_paths }}" + + # TASK [set_fact] ***************************************************** + # task path: /home/brad/github/dotbracket/site.yaml:17 + # ok: [localhost] => changed=false + # ansible_facts: + # as_filter: + # b.c.d[0]: 0 + # b.c.d[1]: 1 + # b.c.e[0]: true + # b.c.e[1]: false + # as_lookup: + # b.c.d[0]: 0 + # b.c.d[1]: 1 + # b.c.e[0]: true + # b.c.e[1]: false + + - name: Use prepend to add the initial variable name + ansible.builtin.set_fact: + as_lookup: "{{ lookup('ansible.utils.to_paths', a, prepend=('a')) }}" + as_filter: "{{ a|ansible.utils.to_paths(prepend='a') }}" + + # TASK [Use prepend to add the initial variable name] ***************** + # ok: [nxos101] => changed=false + # ansible_facts: + # as_filter: + # a.b.c.d[0]: 0 + # a.b.c.d[1]: 1 + # a.b.c.e[0]: true + # a.b.c.e[1]: false + # as_lookup: + # a.b.c.d[0]: 0 + # a.b.c.d[1]: 1 + # a.b.c.e[0]: true + # a.b.c.e[1]: false + + + #### Using a complex object + + - name: Make an API call + uri: + url: "https://nxos101/restconf/data/openconfig-interfaces:interfaces" + headers: + accept: "application/yang.data+json" + url_password: password + url_username: admin + validate_certs: False + register: result + delegate_to: localhost + + - name: Flatten the complex object + set_fact: + flattened: "{{ result.json|ansible.utils.to_paths }}" + + # TASK [Flatten the complex object] ******************** + # ok: [nxos101] => changed=false + # ansible_facts: + # flattened: + # interfaces.interface[0].config.enabled: 'true' + # interfaces.interface[0].config.mtu: '1500' + # interfaces.interface[0].config.name: eth1/71 + # interfaces.interface[0].config.type: ethernetCsmacd + # interfaces.interface[0].ethernet.config['auto-negotiate']: 'true' + # interfaces.interface[0].ethernet.state.counters['in-crc-errors']: '0' + # interfaces.interface[0].ethernet.state.counters['in-fragment-frames']: '0' + # interfaces.interface[0].ethernet.state.counters['in-jabber-frames']: '0' + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this lookup: + +.. raw:: html + + + + + + + + + + + + +
KeyReturnedDescription
+
+ _raw + +
+ - +
+
+
A dictionary of key value pairs
+
The key is the path
+
The value is the value
+
+
+

+ + +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. diff --git a/galaxy.yml b/galaxy.yml index 8f4180d..24a96ad 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,23 +1,13 @@ -# See https://docs.ansible.com/ansible/latest/dev_guide/collections_galaxy_meta.html - -namespace: community -name: FIXME -version: 0.1.0 -readme: README.md +--- authors: - - YOUR NAME (github.com/YOURGITHUB) -description: null -license_file: COPYING -tags: -# tags so people can search for collections https://galaxy.ansible.com/search -# tags are all lower-case, no spaces, no dashes. - - example1 - - example2 -repository: https://github.com/ansible-collections/community.REPO_NAME -#documentation: https://github.com/ansible-collection-migration/community.REPO_NAME/tree/main/docs -homepage: https://github.com/ansible-collections/community.REPO_NAME -issues: https://github.com/ansible-collections/community.REPO_NAME/issues -build_ignore: -# https://docs.ansible.com/ansible/devel/dev_guide/developing_collections.html#ignoring-files-and-folders - - .gitignore - - changelogs/.plugin-cache.yaml + - Ansible Community +license_file: LICENSE +name: utils +namespace: ansible +description: Ansible Collection with utilities to ease the management, manipulation, and validation of data within a playbook +readme: README.md +repository: https://github.com/ansible-collections/ansible.utils +tags: [networking, security, cloud, utilities, data, validation] +# NOTE(pabelanger): We create an empty version key to keep ansible-galaxy +# happy. We dynamically inject version info based on git information. +version: null diff --git a/meta/runtime.yml b/meta/runtime.yml index 2ee3c9f..28d1d3a 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -1,2 +1,2 @@ --- -requires_ansible: '>=2.9.10' +requires_ansible: '>=2.9.10,<2.11' diff --git a/plugins/filter/paths.py b/plugins/filter/paths.py index fea97ff..03104e3 100644 --- a/plugins/filter/paths.py +++ b/plugins/filter/paths.py @@ -17,24 +17,30 @@ from ansible.module_utils.common._collections_compat import ( MutableMapping, ) -from ansible_collections.ansible.utils.plugins.module_utils.generate_paths import ( - generate_paths, +from ansible_collections.ansible.utils.plugins.module_utils.path_utils import ( + to_paths, + get_path, ) from jinja2.filters import environmentfilter -def to_paths(obj, prepend=None): - return generate_paths(obj, prepend) +def _to_paths(*args, **kwargs): + """ Convert complex objects to paths. [See examples](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.to_paths_lookup.rst) + """ + return to_paths(*args, **kwargs) @environmentfilter -def get_path(environment, vars, path): - string_to_variable = "{{ %s }}" % path - return environment.from_string(string_to_variable).render(**vars) +def _get_path(*args, **kwargs): + """ Get value using path. [See examples](https://github.com/ansible-collections/ansible.utils/blob/main/docs/ansible.utils.get_path_lookup.rst) + """ + kwargs["environment"] = args[0] + args = args[1:] + return get_path(*args, **kwargs) class FilterModule(object): - """ Network filter """ + """ path filters """ def filters(self): - return {"to_paths": to_paths, "get_path": get_path} + return {"to_paths": _to_paths, "get_path": _get_path} diff --git a/plugins/lookup/get_path.py b/plugins/lookup/get_path.py new file mode 100644 index 0000000..2d46d0b --- /dev/null +++ b/plugins/lookup/get_path.py @@ -0,0 +1,174 @@ +# -*- 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 get_path lookup plugin +""" +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +DOCUMENTATION = """ + lookup: get_path + author: Bradley Thornton (@cidrblock) + version_added: "1.0" + short_description: Retrieve the value in a variable using a path + description: + - Use a C(path) to retreive a nested value from a C(var) + - C(get_path) is also available as a C(filter_plugin) for convenience + options: + _terms: + description: The values below provided in the order C(var), C(path), C(wantlist=). + required: True + var: + description: The variable from which the value should be extraced + type: raw + required: True + path: + description: > + The C(path) in the C(var) to retrieve the value of. + The C(path) needs to a be a valid jinja path + type: str + required: True + wantlist: + description: > + If set to C(True), the return value will always be a list + This can also be accomplished 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""" +- ansible.builtin.set_fact: + a: + b: + c: + d: + - 0 + - 1 + e: + - True + - False + +- name: Retrieve a value deep inside a using a path + ansible.builtin.set_fact: + as_lookup: "{{ lookup('ansible.utils.get_path', a, path) }}" + as_filter: "{{ a|ansible.utils.get_path(path) }}" + vars: + path: b.c.d[0] + +# TASK [ansible.builtin.set_fact] ************************************* +# ok: [nxos101] => changed=false +# ansible_facts: +# as_filter: '0' +# as_lookup: '0' + + +#### Working with hostvars + +- name: Retrieve a value deep inside all of the host's vars + ansible.builtin.set_fact: + as_lookup: "{{ lookup('ansible.utils.get_path', look_in, look_for) }}" + as_filter: "{{ look_in|ansible.utils.get_path(look_for) }}" + vars: + look_in: "{{ hostvars[inventory_hostname] }}" + look_for: a.b.c.d[0] + +# TASK [Retrieve a value deep inside all of the host's vars] ********** +# ok: [nxos101] => changed=false +# ansible_facts: +# as_filter: '0' +# as_lookup: '0' + + +#### Used alongside ansible.utils.to_paths + +- name: Get the paths for the object + ansible.builtin.set_fact: + paths: "{{ a|ansible.utils.to_paths(prepend='a') }}" + +- name: Retrieve the value of each path from vars + ansible.builtin.debug: + msg: "The value of path {{ path }} in vars is {{ value }}" + loop: "{{ paths.keys()|list }}" + loop_control: + label: "{{ item }}" + vars: + path: "{{ item }}" + value: "{{ vars|ansible.utils.get_path(item) }}" + +# TASK [Get the paths for the object] ********************************* +# ok: [nxos101] => changed=false +# ansible_facts: +# paths: +# a.b.c.d[0]: 0 +# a.b.c.d[1]: 1 +# a.b.c.e[0]: true +# a.b.c.e[1]: false + +# TASK [Retrieve the value of each path from vars] ******************** +# ok: [nxos101] => (item=a.b.c.d[0]) => +# msg: The value of path a.b.c.d[0] in vars is 0 +# ok: [nxos101] => (item=a.b.c.d[1]) => +# msg: The value of path a.b.c.d[1] in vars is 1 +# ok: [nxos101] => (item=a.b.c.e[0]) => +# msg: The value of path a.b.c.e[0] in vars is True +# ok: [nxos101] => (item=a.b.c.e[1]) => +# msg: The value of path a.b.c.e[1] in vars is False + + +#### Working with complex structures + +- name: Retrieve the current interface config + cisco.nxos.nxos_interfaces: + state: gathered + register: interfaces + +- name: Get the description of several interfaces + ansible.builtin.debug: + msg: "{{ rekeyed|ansible.utils.get_path(item) }}" + vars: + rekeyed: + by_name: "{{ interfaces.gathered|ansible.builtin.rekey_on_member('name') }}" + loop: + - by_name['Ethernet1/1'].description + - by_name['Ethernet1/2'].description + +# TASK [Get the description of several interfaces] ******************** +# ok: [nxos101] => (item=by_name['Ethernet1/1'].description) => +# msg: Configured by Ansible +# ok: [nxos101] => (item=by_name['Ethernet1/2'].description) => +# msg: Configured by Ansible Network + +""" + +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.path_utils import ( + get_path, +) + + +class LookupModule(LookupBase): + def run(self, terms, variables, **kwargs): + kwargs["environment"] = self._templar.environment + if isinstance(terms, dict): + terms.update(kwargs) + res = get_path(**terms) + else: + res = get_path(*terms, **kwargs) + if not isinstance(res, list): + return [res] + return res diff --git a/plugins/lookup/to_paths.py b/plugins/lookup/to_paths.py new file mode 100644 index 0000000..be8d2c3 --- /dev/null +++ b/plugins/lookup/to_paths.py @@ -0,0 +1,157 @@ +# -*- 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 to_paths lookup plugin +""" +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +DOCUMENTATION = """ + lookup: to_paths + author: Bradley Thornton (@cidrblock) + version_added: "1.0" + short_description: Flatten a complex object into a dictionary of paths and values + description: + - Flatten a complex object into a dictionary of paths and values. + - Paths are dot delimited whenever possible + - Brakets are used for list indicies and keys that contain special characters + - C(to_paths) is also available as a filter plugin + options: + _terms: + description: The values below provided in the order C(var), C(prepend=), C(wantlist=). + required: True + var: + description: The value of C(var) will be will be used. + type: raw + required: True + prepend: + description: Prepend each path entry. Useful to add the initial C(var) name. + type: str + required: False + wantlist: + description: > + If set to C(True), the return value will always be a list. + This can also be accomplished 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 + +- ansible.builtin.set_fact: + a: + b: + c: + d: + - 0 + - 1 + e: + - True + - False + +- ansible.builtin.set_fact: + as_lookup: "{{ lookup('ansible.utils.to_paths', a) }}" + as_filter: "{{ a|ansible.utils.to_paths }}" + +# TASK [set_fact] ***************************************************** +# task path: /home/brad/github/dotbracket/site.yaml:17 +# ok: [localhost] => changed=false +# ansible_facts: +# as_filter: +# b.c.d[0]: 0 +# b.c.d[1]: 1 +# b.c.e[0]: true +# b.c.e[1]: false +# as_lookup: +# b.c.d[0]: 0 +# b.c.d[1]: 1 +# b.c.e[0]: true +# b.c.e[1]: false + +- name: Use prepend to add the initial variable name + ansible.builtin.set_fact: + as_lookup: "{{ lookup('ansible.utils.to_paths', a, prepend=('a')) }}" + as_filter: "{{ a|ansible.utils.to_paths(prepend='a') }}" + +# TASK [Use prepend to add the initial variable name] ***************** +# ok: [nxos101] => changed=false +# ansible_facts: +# as_filter: +# a.b.c.d[0]: 0 +# a.b.c.d[1]: 1 +# a.b.c.e[0]: true +# a.b.c.e[1]: false +# as_lookup: +# a.b.c.d[0]: 0 +# a.b.c.d[1]: 1 +# a.b.c.e[0]: true +# a.b.c.e[1]: false + + +#### Using a complex object + +- name: Make an API call + uri: + url: "https://nxos101/restconf/data/openconfig-interfaces:interfaces" + headers: + accept: "application/yang.data+json" + url_password: password + url_username: admin + validate_certs: False + register: result + delegate_to: localhost + +- name: Flatten the complex object + set_fact: + flattened: "{{ result.json|ansible.utils.to_paths }}" + +# TASK [Flatten the complex object] ******************** +# ok: [nxos101] => changed=false +# ansible_facts: +# flattened: +# interfaces.interface[0].config.enabled: 'true' +# interfaces.interface[0].config.mtu: '1500' +# interfaces.interface[0].config.name: eth1/71 +# interfaces.interface[0].config.type: ethernetCsmacd +# interfaces.interface[0].ethernet.config['auto-negotiate']: 'true' +# interfaces.interface[0].ethernet.state.counters['in-crc-errors']: '0' +# interfaces.interface[0].ethernet.state.counters['in-fragment-frames']: '0' +# interfaces.interface[0].ethernet.state.counters['in-jabber-frames']: '0' + + +""" + +RETURN = """ + _raw: + description: + - A dictionary of key value pairs + - The key is the path + - The value is the value +""" + +from ansible.plugins.lookup import LookupBase +from ansible_collections.ansible.utils.plugins.module_utils.path_utils import ( + to_paths, +) + + +class LookupModule(LookupBase): + def run(self, terms, variables, **kwargs): + if isinstance(terms, dict): + terms.update(kwargs) + res = to_paths(**terms) + else: + res = to_paths(*terms, **kwargs) + if not isinstance(res, list): + return [res] + return res diff --git a/plugins/module_utils/generate_paths.py b/plugins/module_utils/path_utils.py similarity index 50% rename from plugins/module_utils/generate_paths.py rename to plugins/module_utils/path_utils.py index 1ffc538..e51bafe 100644 --- a/plugins/module_utils/generate_paths.py +++ b/plugins/module_utils/path_utils.py @@ -17,8 +17,40 @@ from ansible.module_utils.common._collections_compat import ( MutableMapping, ) +# 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 get_path(var, path, environment, wantlist=False): + """ Get the value of a path within an object + + :param var: The var from which the value is retrieved + :type var: should be dict or list, but jinja can sort that out + :param path: The path to get + :type path: should be a string but jinja can sort that out + :param environment: The jinja Environment + :type environment: Environment + :return: The result of the jinja evaluation + :rtype: any + """ + string_to_variable = "{{ %s }}" % path + result = environment.from_string(string_to_variable).render(**var) + if wantlist: + return [result] + return result + + +def to_paths(var, prepend=False, wantlist=False): + if prepend: + if not isinstance(prepend, str): + raise AnsibleError("The value of 'prepend' must be a sting.") + var = {prepend: var} -def generate_paths(nested_json, prepend): out = {} def flatten(data, name=""): @@ -38,8 +70,7 @@ def generate_paths(nested_json, prepend): else: out[name] = data - if prepend: - flatten({prepend: nested_json}) - else: - flatten(nested_json) + flatten(var) + if wantlist: + return [out] return out diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt new file mode 100644 index 0000000..71674f0 --- /dev/null +++ b/tests/sanity/ignore-2.10.txt @@ -0,0 +1 @@ +plugins/module_utils/path_utils.py pylint:ansible-bad-module-import # file's use is limited to filter and lookups on control node diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt new file mode 100644 index 0000000..71674f0 --- /dev/null +++ b/tests/sanity/ignore-2.9.txt @@ -0,0 +1 @@ +plugins/module_utils/path_utils.py pylint:ansible-bad-module-import # file's use is limited to filter and lookups on control node