Introduction

This is a collection of filters, reusable includes and other useful components for use with Ansible.

The latest version can be obtained from github.

The recommended way of using this collection is to include it as a git submodule into your Ansible configuration(s), and adjust plugin search paths in ansible.cfg.

Example playbooks to demonstrate how to use the extensions provided here are included in the playbooks directory. To run all of them you can execute ansible-playbook playbooks/all.yml

The general assumption for our deployments is that we do not have valid directory sources to pull information from when setting up a new infrastructure, and that directory sources may be part of our infrastructure. To some extend this can be done with just a plain hosts inventory and shared group variables - as was originally done in our deployments due to shortcomings of Ansible at the time. A better approach nowadays is a custom inventory plugin - this allows easier validation of configuration data as well as augmentation from partial additional directory sources.

The inventory plugin still transforms configuration data into a set of variables, to be consumed by the other roles - so the majority of our components can also be used without our inventory plugin, by manually configuring the variables specified in the role documentation.

Both this role and the inventory plugin also support transforming a more generic configuration syntax into platform specific data structures or actions - making it easy to mix different Linux distributions or even Windows and MacOS systems with minimal platform specific code in your playbooks. Many of the reusable includes in this role are for this purpose. Our basic-host role packages many of those into a configurable variant to bring the basic functionality of any host into the expected state - this includes management access, network configuration, basic package installation as well as hooks into custom adjustments.

Deployment strategy

Recommended Ansible setup

The following example shows the recommended directory structure for the repository containing the Ansible configuration:

├── .gitignore
├── ansible.cfg
├── group_vars
│   ├── all.yml
│   ├── dotnet_workers.yml
│   ├── linux_workers.yml
│   ├── macos_workers.yml
│   └── windows_workers.yml
├── host_vars
│   ├── mac01.yml
│   ├── win03.yml
│   └── ubuntu01.yml
├── local_roles
│   └── README
├── playbooks
│   ├── access.yml -> ../roles/data-utilities/playbooks/access.yml
│   ├── files
│   │   ├── authorized_keys.d
│   │   │   ├── user_one
│   │   │   └── user_two
│   │   ├── cleanup-script.sh
│   │   ├── config.json
│   │   └── sample.service
│   ├── handlers
│   │   └── main.yml
│   ├── site.yml
│   ├── tasks
│   │   ├── custom-task1.yml
│   │   └── custom-task2.yml
│   └── templates
│       ├── sample-template1.j2
│       └── sample-template2.j2
├── roles
│   ├── backup
│   │   ├── files
│   │   └── [..]
│   ├── basic-host
│   │   ├── files
│   │   └── [..]
│   ├── data-utilities
│   │   ├── bin
│   │   └── [..]
│   [..]
├── README.org
└── site.yaml

.gitignore

*.pyc
*.retry
/playbooks/host-keys
ansible.log

ansible.cfg

This is a suggested minimal ansible.cfg, though you might want to start from an ansible example for the included comments:

[defaults]
inventory      = site.yaml
gather_timeout = 30
roles_path    = local_roles:external_roles:roles
inventory_plugins     = roles/data-utilities/inventory_plugins
filter_plugins     = roles/ansible-data-utilities/filter_plugins

[inventory]
enable_plugins = site_yaml
unparsed_is_failed = True

[ssh_connection]
pipelining = True
scp_if_ssh = smart

[site_yaml]
dynamic_groups = True

The following settings for the default section often also make sense:

remote_tmp     = ~/.ansible/tmp
local_tmp      = ~/.ansible/tmp
forks          = 20
gathering      = smart

group_vars, host_vars

A list of files containing group variables, which typically at least should contain all.yml. See Ansible inventory introduction for more details.

While files without file extension work using either .yml or .yaml is strongly recommended.

external_roles

A symlink to a directory of checked out roles or unpacked roles. If strict version checking is not required this is an easy approach for simple setups as well as for development. For controlled setups the local_roles approach described in the next section typically is better.

local_roles

A placeholder directory for temporarily storing or linking roles to for development. It can be created empty with a placeholder README similar to the following:

This directory is for holding roles during development, before importing a new
release into the roles subfolder.

This directory can either hold subdirectories of direct git clones, or symlinks
to role directories. Typically having a clone of the upstream role somewhere,
and just linking to it is the easier way:

  ln -s /path/to/roles/sample-role .

or from the ansible root:

  ln -s /path/to/roles/sample-role local_roles/

Don't forget to remove links/directories when importing a newly released role!

playbooks

This directory contains the infrastructure playbooks and related files.

roles

This directory holds roles versioned together with the Ansible configuration. There are two approaches for managing this:

  • synchronising from a git source tree or a release tarball
  • adding roles as git submodules

The submodule approach is recommended.

README

A README in the preferred markup format for the project with at least basic information on how to run Ansible and update roles is recommended.

site.yaml

The site configuration. See the inventory plugin documentation for details.

Configuring a host for ansible

New hosts - no matter the OS - should start out in a small configuration, ideally without much differences between deployments. To achieve that (and to reduce the manual labour) infrastructure for unattended installations should be used whenever possible.

For Linux the recommended setup is netbooting with AutoYAST or Kickstart, with management keys pre-loaded for the root user. This allows easy passwordless running of the access playbook directly after installation.

For Windows the recommended setup is a customised ISO with management user pre-generated, SSH pre-installed and SSH keys pre-loaded. Setting up the management user with correct permissions during the initial ansible run is slightly more complicated on Windows - so making sure this user exists during bootstrap simplifies things.

This page collects information related to managing the automated installation systems, as well as scripts to quickly take over a system which could not be bootstrapped automatically.

Generic initial setup via SSH

macOS SSH setup

On macOS images without custom provisioning remote login needs to be enabled. For modern macOS versions this typically also requires granting full disk access privileges:

% sudo launchctl load -w /System/Library/LaunchDaemons/ssh.plist
% sudo systemsetup -setremotelogin on

Ansible will use python3, which is not installed per default, and comes with the Command Line Developer Tools. If not installed a first Ansible run may fail with an interactive prompt on the desktop to install those. After finishing the installer the Ansible run should succeed.

Windows SSH setup

The easiest way for managing Windows via Ansible is by just making sure SSH is available on the Windows server. This can be done by a custom scripted Windows installation, or by following this section.

The deploy-ssh powershell script can be used both for installing SSH during an unattended Windows installation (with the SSH installer provided on the media), as well as enable it later on.

It will search for the installer OpenSSH-Win64.zip in c:\ci, the current directory and in the current users Download directory, in that order. If not found it will try to download it from the Win32-OpenSSH release page. It should be downloaded and executed in a powershell session with elevated privileges:

> Invoke-WebRequest -Uri https://raw.githubusercontent.com/aardsoft/ansible-data-utilities/master/doc/deploy-ssh.ps1 -OutFile deploy-ssh.ps1
> ./deploy-ssh.ps1

If the execution policy for scripts has not been changed from the default it may be necessary to bypass it for running the script:

> powershell -executionpolicy bypass -File ./deploy-ssh.ps1

On old Windows versions it may be required to force Powershell to use recent TLS mechanisms for the download - set the following if above throws SSL errors:

> [Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls"

If using RDP to connect to the server the connection may get terminated while running deploy-ssh.ps1, though SSH access should be reachable from that point:

$ ssh -o PubkeyAuthentication=no Administrator@windows-system

Depending on the Windows version it may now be possible to also run an access playbook, or manual setup of SSH keys may be required. Running the playbook will handle setting up authorized keys as on other platforms - when manually installing an authorized_keys file for access setup note that the default configuration uses ProgramData\ssh\administrators_authorized_keys for any users in the administrators group. For other users the location is in .ssh\authorized_keys in the root of their home directory.

Windows custom image

Creating a custom image allows adding or removing components to the regular Windows installation, and pre-load scripts required for taking over a host by Ansible.

The windows-image powershell script can be used to mostly automate image creation. While it will ask for any missing parameters it is recommended to create an answer file. The following example contains all possible variables:

$iso_path="c:\Users\example\Documents\Windows.iso"
$new_iso_path="c:\Users\example\Documents\Windows_unattended.iso"
$virtio_iso_path="c:\Users\example\Documents\virtio.iso"
$image_path="c:\unattended_image"
$ci_source_path="c:\ci"
$ci_target_dir="\ci"
$autounattend_path="c:\Autounattend.xml"
$os_index=6
$wait_for_manual=$true
$remove_apps=@(
  "Microsoft.BingWeather"
  "Microsoft.Getstarted"
)

It should be saved as powershell script (with ending .ps1), and can be passed to the windows-image script either as first argument, or through the environment variable CI_ISO_PARAMETERS.

autounattend_path is the path to a that can be used as Autounattend.xml for an unattended installation. If this variable is not set, but the CI script directory described below contains Autounattend.xml this will be used without a need for configuration.

os_index is the index of the Windows variant to be used for multi variant source images - if unspecified the script will list all available variants before prompting. The specified index will be converted to a single variant install.wim. This setting is only relevant on Windows media containing an install.esd file - for newer media shipping a multi variant install.wim the file will be used directly, and variant selection happens at installation time.

remove_apps is a list of preinstalled Windows applications that should be removed from the image. The list of applications in mount_path can be obtained with Get-AppxProvisionedPackage -path mount-path|Format-Table.

wait_for_manual makes the script pause before unmounting boot and install images, allowing manual injection of components.

ci_source_path is the path to a directory containing the files to be injected into the image. A detailed suggestion on how to use this directory is in the section below.

ci_target_dir is the path to a directory relative to the mountpoint of the install media to place the files. \ci would place them at the root of the install drive after installation (typically c:\ci)

virtio_iso_path is the path to an ISO image containing virtio drivers. If this is specified (and exists) all drivers on this ISO will be injected into the new image. Newer Windows versions require signed drivers - it is recommended to use WHQL signed drivers. One option for this is to use the drivers extracted from rocky linux.

The script expects a Windows ISO file, which can be created by

At the time of writing Arm64 images can only be obtained via the insider program or uupdump. The same four options exists for Windows 10, links should be trivially obtainable by searching the microsoft site.

  • CI scripts

Windows unattended image

Creating an unattended Windows image requires following the stops for creating a custom Windows image in the section above first.

Next the Windows ADK needs to be installed for creating an answer file. Note that creating Windows 10 or 32-bit images may require using legacy ADK versions. Selecting Deployment Tools in the features list should be sufficient.

Filters

For generic information on filters please read the Ansible filter documentation.

boolconv

This filter takes most of the inputs possible in Ansible to look like a bool, and converts them to a controlled string representation. Recognised inputs are all data which become one of true, false, yes, no, on, off, 0 or 1 when passed through Ansible variable expansion into Python.

This solves the problem of Ansible auto-converting yes/no to booleans, and eliminates the chance of having malformed values in configuration files due to forgotten quotes in the variable declaration.

A short example playbook demonstrates this problem with a template containing both defaults and sanitised values:

- template:
    src: boolconv.j2
    dest: .out/boolconv
  vars:
    var1: yes
    var2: "yes"
    var3: true
{{var1}} {{var1|boolconv("yesno")}}
{{var2}} {{var2|boolconv("yesno")}}
{{var3}} {{var3|boolconv("yesno")}}

The output shows that Ansible converts yes to a bool, which then becomes True when converted back to a string.

True yes
yes yes
True yes

Note that this also can be a problem for configuration files accepting true/false, but expect them to be lower case. To avoid this issue as well this filter allows controlling case of the output as well. Valid arguments are:

  • truefalse
  • TrueFalse
  • TRUEFALSE
  • yesno
  • YesNo
  • YESNO
  • onoff
  • ONOFF
  • int
  • enableddisabled

The default if no argument is specified is truefalse. With a wrong argument as well as inputs not recognisable as one of the values listed above the return value is undefined (typically results in an empty string in Ansible).

ipv6_explode

Explode an IPv6 address, i.e., transforms something like 2a0b:5c81:300:9254::1 into 2a0b:5c81:0300:9254:0000:0000:0000:0001. This will always return the IP address, but accepts arguments with and without prefix.

Includes for other roles

Generic includes

add_ssl_files

This include handles management of SSL keys and certificates. It can pull keys or certificates from a password store, or generate missing files.

It takes the following variables:

  • ssl_size, the size of the SSL key. Defaults to 2048
  • ssl_key, the path to the key file
  • ssl_certificate, the path to the certificate file
  • ssl_key_passdb, a passdb entry containing the SSL key
  • ssl_certificate_passdb, a list of passdb entries containing SSL certificates. This allows easy inclusion of intermediaries as well.

If ssl_certificate is set without a passdb source a self signed certificate will be generated, if it is missing on disk. This is mostly useful when using services like letsencrypt - this allows bringing up the services with temporary certificates, to be replaced as soon as a system is operable enough to request proper certificates.

Example usage:

- include_role:
    name: data-utilities
    tasks_from: add_ssl_files
  vars:
    ssl_key: /etc/ssl/private/test.key
    ssl_certificate: /etc/ssl/private/test.pem
    ssl_size: 4096

When using passdb entries for key and certificates it also is possible to have keys and certificates combined in a single file, specified by ssl_key:

- include_role:
    name: data-utilities
    tasks_from: add_ssl_files
  vars:
    ssl_key: /etc/ssl/private/test.pem
    ssl_key_passdb: test-key
    ssl_certificate_passdb:
      - test-cert
      - intermediary

Using a single file without passdb entries will result in certificate generation being skipped, which quite likely will lead to failures.

configure_gpg_users

This include configures gpg for one or more users, with optional pkcs11-scd support, expecting configuration in a variable named gpg_users. The basic-host role will automatically set this up if the variable is defined.

Sample configuration for user accounts user1 and user2:

gpg_users:
  user1:
    gnupg-pkcs11-scd:
    gpg-agent.conf.extra:
      - allow-emacs-pinentry
      - debug-level advanced
  user2:
    gpg-agent:
      debug: advanced
      verbose:
    gpg.conf:
      - no-secmem-warning
  • dirmngr.conf

    A lost of options to use in dirmngr.conf. If omitted the following default configuration will be used:

    keyserver hkps://keys.openpgp.org
    
  • dirmngr.conf.extra

    A list of extra options to add to dirmngr.conf

  • gpg.conf

    A list of options to use in gpg.conf. If omitted the following default configuration will be used:

    keyserver-options auto-key-retrieve
    use-agent
    no-secmem-warning
    
  • gpg.conf.extra

    A list of extra options to add to gpg.conf

  • gpg-agent

    A list of flags changing the default configuration.

    • debug takes the values of debug-level described for gpg-agent. If gnupg-pkcs11-scd is used anything other than none here will enable debug logging there as well.
    • verbose will enable verbose for gpg-agent and gnupkc-pkcs11-scd unless set to false.
  • gpg-agent.conf

    A list of options to use in gpg-agent.conf. If omitted the following default configuration will be used:

    log-file $HOME/.gnupg/log/gpg-agent.log
    enable-ssh-support
    max-cache-ttl 172800
    default-cache-ttl 86400
    default-cache-ttl-ssh 10800
    write-env-file $HOME/.gpg-agent-info
    
  • gpg-agent.conf.extra

    A list of extra options to add to gpg-agent.conf

  • gnupg-pkcs11-scd

    If present will add scdaemon-program pointing to gnupg-pkcs11-scd to gpg-agent configuration, and copy gnupg-pkcs11-scd configuration.

    This setting can have the subkeys library (default: /usr/lib64/p11-kit-proxy.so) and program (default: /usr/bin/gnupg-pkcs11-scd).

configure_gpg

This include configures gpg for a single user, and expects to be run as that user. The include for multiple users just uses this include in a loop. The data structure expected as gpg is therefore a single user entry of the structure described there.

install_packages

This include handles package installation for the provided packages on the following distribution/package manager combinations:

  • SuSE with zypper
  • RedHat/CentOS/Fedora with yum
  • RedHat/CentOS/Fedora with dnf
  • Debian/Ubuntu with apt

It takes the following variables:

  • packages, a list of packages to install.
  • install_retries, the number of retries if package installation fails. Defaults to 3.

Example usage:

- include_role:
    name: data-utilities
    tasks_from: install_packages
  vars:
    install_retries: 10
    packages:
      - nmap

manage_service

This include abstracts commonly used service settings for systemd services and launchd services. The setting names are modeled after systemd services, lowercased with underscores between words.

Additionally state can be set to one of the values supported by the Ansible systemd module. It defaults to restarted, though the state change is only triggered if the service file changes on disk. Due to shortcomings of the OS X launchd every value not abesnt will lead to the service being started according to the service file.

For OS X and Linux the following keys are supported:

name service name, mandatory
exec_start executable and arguments. On OS X this gets split for ProgramArguments
user user name, if service should not run as root
group group name, if service should not run as root/wheel
restart restart settings. For OS X, this maps to KeepAlive: If unspecified or 'always' it is true
  on-failure and on-abort map to Crashed
  on-success maps to SuccessfulExit
standard_error redirection of standard error. On OS X this should be a path to a file
stardard_output redirection of standard out. On OS X this should be a path to a file
environment key/value list of environment variables. On OS X, if it does not contain PATH, runtime path + homebrew path is set

Linux additionally supports:

  • exec_start_pre
  • exec_start_post
  • restart_sec
  • description
  • wanted_by, defaulting to multi-user.target
  • type
- include_role:
    name: data-utilities
    tasks_from: manage_service
  vars:
    service:
      name: test
      state: absent

- include_role:
    name: data-utilities
    tasks_from: manage_service
  vars:
    service:
      name: test
      exec_start: /path/to/binary
      environment:
        foo: bar

Includes mainly used by basic-host

This section describes includes used by the basic-host role. The documentation here is mainly useful for the variables used to adjust their behaviour - but also may be useful for sites where the complete configuration applied by basic-host is not desirable.

check_versions

This include checks if the running Ansible version matches the version range this role has been tested with. Additionally it also exports the variable data_utilities_version, and sets up some default variables. This also implicitely registers filter paths with Ansible.

The basic-host role includes this file, so unless another role needs to use a higher data-utilities version than basic-host was tested for including this file should not be necessary. When not using basic-host, or using multiple roles before basic-host this include should be included early on in the play.

For checking if data utilities is available in the correct version the data_utilities_minver variable can be set:

- include_role:
    name: data-utilities
    tasks_from: check_versions
  vars:
    data_utilities_minver: 0.1
  tags:
    - base_config
    - access_setup

manage_hostname

This include tries to set the system hostname. On Windows this may require a reboot - execution continues once the system is reachable again.

For setting the hostname either the hostname variable is used, or - if missing - the hostname is generated from the inventory hostname.

manage_motd

This adds or removes files for motd. Variables controlling this should be set in group or host variables.

motd_templates controls templates for all hosts. motd_templates_<hostname> controls templates for the specific host only. motd_templates_<groupname> are added if the host is in that group.

The variables should be a dict, with optional keys filename and state. If state is absent the file will be removed, added for all other values. filename controls the filename in the motd directory - if unset the key will be used. The template is always looked up as the key with .j2 appended.

The basic-host role includes this file for all Linux systems - so as long as this role is used there should be no need to include this file.

motd_templates:
  motd_default:

motd_templates_dummy:
  motd_dummy:
  motd_gone:
    state: absent

This example would add motd_default.j2 to all hosts, and motd_dummy.j2 to a host named or in a group called dummy, and remove motd_gone from that host.

manage_nameservices

This include configures name services for a system - this mainly, but not only, covers DNS. Windows currently is not supported.

Nameservers from a list in the variable nameservers are used, if available. It usually makes sense to set a default in groupvars for all, and override it for other systems/groups.

There also is some legacy support for copying in prefilled resolv.conf templates based on resolv_location and site_region settings - this was implemented due to legacy Ansible restriction, and should not be used in new deployments.

manage_time

This include configures timezone and other time related settings. On Windows this also sets NTP servers. For Linux this is handled in a separate ntp-client role.

NTP servers are read from a list in the ntp_servers variable. This typically should be set in the default groupvars for all, with overrides as needed.

For Windows the timezone is configured in host_timezone_win, using Microsofts time zone names. For other system standard TZ database names in the host_timezone variable are used.

For all systems the hardware clock is set to UTC.

setup_passdb

This initialises the default password store. Variables controlling the setup should be set in group or host variables. the basic-host role includes this file - so as long as this role is used there should be no need to include this file.

Available variables are:

  • passdb, defaulting to passwordstore
  • passdb_password_length, default 20. This is used when creating passwords from within Ansible.
  • passdb_password_create, bool, default True. Configures if Ansible is allowed to create missing passwords.
  • passdb_extra_arg, default = create={{passdb_password_create}} length={{passdb_password_length}}=
  • passdb_check_entry, default empty. Allows setting a record to check if passdb is working. This is useful for writing playbooks executable by both full access admins and people without access to some passwords. A sensible value typically is the entry for the default root password.

The variable default_passdb_available will be set by this include:

  • True if no passdb_check_entry has been configured. This can lead to errors when executed without correct passdb access permissions.
  • True if passdb_check_entry is accessible.
  • False if passdb_check_entry is inaccessible.

Tasks/roles should use this variable to guard sections prompting for credentials, or skip execution completely.

  • using 1password as passdb backend

    Using 1password requires the 1password CLI to be installed and configured to ask for credentials when queried in an unlocked state - like connecting it to the desktop application. passdb then must be set to community.general.onepassword, passdb_extra_arg to "".

    Currently limits to specific vaults or other settings supported by the module can't be configured in this role - see this issue for details.

    Passwords should be saved as items of type Password. The name of the item is used for the lookup, the content of the field password is retrieved. All other fields will be ignored when filled.

Inventory plugin

data format description

group

While optional it is sensible to pre-define groups with host filters to avoid mistakes

sites

For infrastructure spanning multiple physical locations hosts or host groups can be allocated to sites.

  • name

    The name of the site.

  • description

    Description of the site.

networks

This will generate multiple helper variables for easier consumption in roles:

  • vlans
  • dhcp_networks