Skip to content

πŸ“₯ Custom filter and test plugins for Ansible

License

Notifications You must be signed in to change notification settings

aisbergg/ansible-plugins

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

32 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Ansible Plugins

This repository contains custom Ansible plugins, that can be used in Ansible Roles and Playbooks.

Note: Only Python 3 is supported since Python 2 has long been EOL. For Ansible to use Python 3 you can specify the path to the Python executable using the variable ansible_python_interpreter=/usr/bin/python3 in your inventory.

Table of contents:


Lookup Plugins

Lookup plugins allow Ansible to access data from outside sources. Lookups are used as follows:

- set_fact:
    # retrieve or generate a random password
    var: "{{ lookup('password', '/tmp/passwordfile') }}"

Install

To install one or more lookups in an Ansible Playbook or Ansible Role, add a directory named lookup_plugins and place the lookups (Python scripts) inside it.

Reference

default4dist

Creates distribution dependent values.

The default4dist lookup looks for a variable by a name prefix in combination with the OS distribution or family name and returns its value. This means, you don't have to create long chains of ansible_distribution == 'Foo' statements to build distribution dependent values.

The lookup will search for variables in form of {prefix}{suffix} or {prefix}_{suffix}. The first found variable will be used or optional combined (dict or list) with a default value. The suffix is constructed in the following order of precedence:

  • {distribution}_{release}_{version}
  • {distribution}_{release}_{major_version}
  • {distribution}_{release}
  • {distribution}_{version}
  • {distribution}_{major_version}
  • {distribution}
  • {familiy}_{release}_{version}
  • {familiy}_{release}_{major_version}
  • {familiy}_{release}
  • {familiy}_{version}
  • {familiy}_{major_version}
  • {familiy}
  • default

OS distribution and family names must be specified in lower case.

Example:

# a simple value that differs on different Linux distributions
myvar: "{{
    'foo' if ansible_distribution == 'Debian' else
    'bar' if ansible_distribution == 'CentOS' and ansible_distribution_release == 'Stream' else
    'bar' if ansible_distribution == 'CentOS' and ansible_distribution_major_version == '8' else
    'baz'
}}"
# can be replaced by:
_myvar_default: baz
_myvar_debian: foo
_myvar_centos_stream: bar
_myvar_centos_8: foobar
myvar: "{{ lookup('default4dist', '_myvar') }}"


# a dictionary that contains some distribution dependent values
myvar: "{{
    {'a': 1, 'b': foo} if ansible_distribution == 'Debian' else
    {'a': 2, 'b': bar} if ansible_distribution == 'CentOS' and ansible_distribution_release == 'Stream' else
    {'a': 3, 'b': bar} if ansible_distribution == 'CentOS' and ansible_distribution_major_version == '8' else
    {'a': 1, 'b': baz}
}}"
# can be replaced by:
_myvar_default: {'a': 1, 'b': baz}
_myvar_debian: {'b': foo}
_myvar_centos_stream: {'a': 2, 'b': bar}
_myvar_centos_8: {'a': 3, 'b': bar}
myvar: "{{ lookup('default4dist', '_myvar', recursive=True) }}"

keepassxc_browser_password

Retrieves a password from an opened KeePassXC database using the KeePassXC Browser protocol.

The plugin allows to automatically load sensitive information from KeePassXC into Ansible, thus can be used as an addition to or even replacement of the Ansible vault. Besides loading passwords for your database for example, you can also load the Ansible become or SSH password and avoid retyping it over and over again.

The KeePass Lookup allows to retrieve password from an opened KeePass database and use them directly in Ansible. The plugin works like any of the KeePass browser plugins, it connects to a local port via HTTP, forms an (cryptographic) association and is then able to retrieve passwords.

The entries in the database need to be properly named. First of all the entries must have a valid URL, because the protocol returns entries by matching a given URL. The URL must consists at least of a scheme and a hostname (e.g. 'https://foo', don't use a random scheme, KeePass doesn't like that). Searching only by an URL might not return a unique result, therefore the results can be trimmed down by adding filters for the entry name or login. See the plugin documentation for more information.

Installation:

The plugin requires the Python keepassxc_browser module. It can be installed in user context like this:

pip install --user keepassxc-browser

Example:

- set_fact:
    # simple password lookup by URL
    var1: "{{ lookup('keepassxc_browser_password', 'https://example.org') }}"
    # password lookup by URL and login name
    # the protocol part 'ansible://' is required to form a valid URL, it doesn't have to be 'https://' or else
    var2: "{{ lookup('keepassxc_browser_password', 'url=ansible://mysql login=root') }}"
    # password lookup by URL and name
    var3: "{{ lookup('keepassxc_browser_password', 'url=ansible://secret name=\"My Secret\"') }}"
    # password lookup by URL and group
    var4: "{{ lookup('keepassxc_browser_password', 'url=ansible://secret group=department_x') }}"
Automatic Login (SSH And Become Password Lookup)

You can use the plugin to lookup the Become and/or SSH passwords on Ansible startup, so you don't have to type these in all the time. There are two things you need to do to make it work:

  1. Add a lookup for your Become and/or SSH passwords:

    # group_vars/all
    _ansible_become_pass: "{{ lookup('keepass_http_password', 'ansible://linux-user login=foo') }}"
    _ansible_ssh_pass: "{{ lookup('keepass_http_password', 'ansible://linux-user login=foo') }}"
  2. Statically evaluate the ansible_ssh_pass and ansible_become_pass in your playbook. This is a necessary step to avoid a relatively "slow" password lookup for every single task, because Ansible won't cache any lookups:

    # playbook.yml
    - hosts: xxx
      pre_tasks:
        - set_fact:
            ansible_ssh_pass: "{{ ansible_ssh_pass | default(_ansible_ssh_pass) | default(omit) }}"
            ansible_become_pass: "{{ ansible_become_pass | default(_ansible_become_pass) | default(omit) }}"
          tags: always
          no_log: true
Automatic Vault Decryption

It is also possible to automate the vault decryption, it requires an additional script to accomplish though. I created a Vault Password Client Script for that purpose, that reuses some of the code of the lookup plugin:

vault-pass-client.py :

import argparse
import sys
from pathlib import Path

from ansible.errors import AnsibleError

RELATIVE_PATH_TO_PLUGIN_DIR = '../../plugins/lookup/'

sys.path.append(str(Path(__file__).parent.joinpath(RELATIVE_PATH_TO_PLUGIN_DIR).resolve()))
from keepassxc_browser import Connection, Identity, ProtocolError
from keepassxc_browser_password import KeePassXCBrowserPasswordLookup

__author__  = 'Andre Lehmann'
__email__   = 'aisberg@posteo.de'
__version__ = '1.1.0'
__license__ = 'MIT'


def main():
    # parse arguments
    parser = argparse.ArgumentParser()
    parser.add_argument('--vault-id', dest='vault_id', required=True, help='The vault ID')
    args = parser.parse_args(sys.argv[1:])

    try:
        lookup = KeePassXCBrowserPasswordLookup()
    except ProtocolError as excp:
        raise AnsibleError("Failed to establish a connection to KeePassXC: {}".format(excp))
    except Exception as excp:
        raise AnsibleError("KeePassXC password lookup execution failed: {}".format(excp))

    try:
        url = 'ansible://ansible-vault'
        filters = dict(login=args.vault_id)
        vault_pass = lookup.get_password(url=url, filters=filters)
    except Exception as ex:
        del lookup
        raise AnsibleError(str(ex))

    sys.stdout.write(vault_pass + '\n')


if __name__ == '__main__':
    main()

The script needs to be saved as *-client.py in order to work. One thing that need to be changed, is the path (RELATIVE_PATH_TO_PLUGIN_DIR) to the plugin dir containing the keepassxc_browser_password lookup plugin. That's done, you can use it as follows:

  1. Save the vault password in KeePassXC:
    • Username: myuser
    • Password: myvaultpass
    • URL: ansible://ansible-vault
  2. Run Ansible: ansible-playbook --vault-id myuser@/path/to/vault-pass-client.py ...

keepass_http_password

Retrieves a password from an opened KeePass database using the KeePass HTTP protocol.

The KeePass Lookup allows to retrieve password from an opened KeePass database and use them directly in Ansible. The plugin works like any of the KeePass browser plugins, it connects to a local port via HTTP, forms an (cryptographic) association and is then able to retrieve passwords.

The entries in the database need to be properly named. First of all the entries must have a valid URL, because the protocol returns entries by matching a given URL. The URL must consists at least of a scheme and a hostname (e.g. 'https://foo', don't use a random scheme, KeePass doesn't like that). Searching only by an URL might not return a unique result, therefore the results can be trimmed down by adding filters for the entry name or login. See the plugin documentation for more information.

This plugin works much like the keepassxc_browser_password plugin and offers similar features.

Installation:

The plugin requires the Python keepasshttp module. You can install it via pip install --user keepasshttp. After that the plugin just needs to be copied into dir lookup_plugins in your Ansible repository.

  1. Install KeePassHttp plugin: https://github.com/pfn/keepasshttp/
  2. Install KeePassHttp Python module: pip install --user keepasshttp
  3. Open KeePass and configure the KeePassHttp plugin to match the schemes: Tools ➞ KeePassHttp Options... ➞ General ➞ Match URL schemes

Example:

- set_fact:
    # simple password lookup by URL
    var1: "{{ lookup('keepass_http_password', 'https://example.org') }}"
    # password lookup by URL and login name
    # the protocol part 'ansible://' is required to form a valid URL, it doesn't have to be 'https://' or else
    var2: "{{ lookup('keepass_http_password', 'url=https://mysql login=root') }}"
    # password lookup by URL and name
    var3: "{{ lookup('keepass_http_password', 'url=https://secret name=\"My Secret\"') }}"

Filter Plugins

Filters are used to transform data inside template expressions. In general filters are used in the following fashion:

# apply a filter to `some_variable`
{{ some_variable | filter }}

# apply a filter with extra arguments
{{ some_variable | filter(arg1='foo', arg2='bar') }}

Install

To install one or more filters in an Ansible Playbook or Ansible Role, add a directory named filter_plugins and place the filters (Python scripts) inside it.

Reference

pbkdf2_hash

Create a password hash using pbkdf2.

Example:

{{ plain_password | pbkdf2_hash(rounds=50000, scheme='sha512') }}

selectattr2

Filter a sequence of objects by applying a test to the specified attribute of each object, and only selecting the objects with the test succeeding.

The built-in Jinja2 filter selectattr fails whenever the attribute is missing in one or more objects of the sequence. The selectattr2 is designed to not fail under such conditions and allows to specify a default value for missing attributes.

Examples:

# select objects, where the attribute is defined
{{ users | selectattr2('name', 'defined') | list }}

# select objects, where the attribute is equal to a value
{{ users | selectattr2('state', '==', 'present', default='present') | list }}

slugify

Convert a string to a slug representation.

The filter converts strings into slugs by removing non alphanumerics, underscores, or hyphens, converting to lower case and stripping leading and trailing whitespace, dashes, and underscores.

Examples:

{{ 'Hello World' | slugify() }} -> 'hello-world'

split

Split a string by a specified seperator string.

Splitting a string with Jinja can already accomplished by executing the split" method on strings, but when you want to use split in combination with "map" for example, you need a filter like this one.

Examples:

# split a simple string
{{ 'Hello World' | split(' ') }} -> ['Hello', 'World']

# split strings in combination with 'map'
{{ ['1;2;3', 'a;b;c'] | map('split', ';') | list }} -> [['1', '2', '3'], ['a', 'b', 'c']]

to_gvariant

Convert a value to GVariant Text Format.

Example:

{{ [1, 3.14, True, "foo", {"bar": 0}, ("foo", "bar")] | to_gvariant() }}
-> [1, 3.14, true, 'foo', {'bar': 0}, ('foo', 'bar')]

Test Plugins

Tests are used to evaluate template expressions and return either True or False. Tests are used as follows:

# using a test on `some_variable`
{% if some_variable is test %}{% endif %}

# a test with extra arguments
{% if some_variable is test(arg1='foo', arg2='bar') %}{% endif %}

Install

To install one or more tests in an Ansible Playbook or Ansible Role, add a directory named test_plugins and place the tests (Python scripts) inside it.

Reference

boolean

Test if a value is of type boolean.

This test plugin can be used until Ansible adapts Jinja2 version 2.11, which comes with this filter built-in (see).

Example:

{% if foo is boolean %}{{ foo | ternary('yes', 'no') }}{% endif %}

list

Test if a value a list or generator type.

Jinja2 provides the tests iterable and sequence, but those also match strings and dicts as well. To determine, if a value is essentially a list, you need to check the following:

value is not string and value is not mapping and value is iterable

This test is a shortcut, which allows to check for a list or generator simply with:

value is list

Example:

{% if foo is list %}{{ foo | join(', ') }}{% endif %}

License

MIT

Author Information

Andre Lehmann (aisberg@posteo.de)

About

πŸ“₯ Custom filter and test plugins for Ansible

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages