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 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') }}"
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.
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) }}"
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') }}"
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:
-
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') }}"
-
Statically evaluate the
ansible_ssh_pass
andansible_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
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:
- Save the vault password in KeePassXC:
- Username:
myuser
- Password:
myvaultpass
- URL:
ansible://ansible-vault
- Username:
- Run Ansible:
ansible-playbook --vault-id myuser@/path/to/vault-pass-client.py ...
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.
- Install KeePassHttp plugin: https://github.com/pfn/keepasshttp/
- Install KeePassHttp Python module:
pip install --user keepasshttp
- 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\"') }}"
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') }}
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.
Create a password hash using pbkdf2.
Example:
{{ plain_password | pbkdf2_hash(rounds=50000, scheme='sha512') }}
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 }}
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 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']]
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')]
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 %}
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.
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 %}
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 %}
MIT
Andre Lehmann (aisberg@posteo.de)