I need to be able to install a MySQL library. Python has 1 package for v2 and another for v3. I need to be able to tell Ansible which package to install.
- name: Ensure MySQL-python is installed
pip:
name: MySQL-python
state: present
become: true
when: python_version is regex("^2.*")
- name: Ensure mysqlclient is installed
pip:
name: mysqlclient
state: present
become: true
when: python_version is regex("^3.*")
The regular expression is valid but Ansible is skipping them both even though this:
- debug:
var: python_version
returns this:
TASK [debug] ****************************************************************************************************************************************************************
ok: [localhost] => {
"python_version": "2.7.10"
}
regex works for me with ansible 2.7.9. The example below
vars:
python_version: "2.7.10"
tasks:
- debug:
msg: Python version 2
when: python_version is regex('^2.*')
gives
"msg": "Python version 2"
Version Comparison is more convenient for complex tests. The example below gives the same result.
- debug:
msg: Python version 2
when:
- python_version is version('2', '>=')
- python_version is version('3', '<')
The test regex is documented in Ansible 2.8 for the first time. In earlier versions, only the tests search and match are documented. In the current source the tests search and match are implemented on top of regex
def match(value, pattern='', ignorecase=False, multiline=False):
return regex(value, pattern, ignorecase, multiline, 'match')
def search(value, pattern='', ignorecase=False, multiline=False):
 return regex(value, pattern, ignorecase, multiline, 'search')
Like Vladimir said.
Another improvement might be the source of python_version. When using gather_facts you can do it without regexp.
---
- hosts: localhost
gather_facts: True
tasks:
- name: do some stuff
debug:
msg: do something because it is python 3
when: ansible_facts.python.version.major is version('3', '=')
- name: do other stuff
debug:
msg: I don't support legacy software
when: ansible_facts.python.version.major is version('2', '=')
Related
I'm trying to replicate the command yum check-update package_name preferably with the Ansible yum module.
It provides an information to what version package would be updated after yum update execution (or ansible equivalent). For example:
root#host: $ yum check-update kernel
[...]
kernel.x86_64 3.10.0-1160.80.1.el7
[...]
root#host: $
I tried various combination of
- name: Xyz
ansible.builtin.yum:
list: updates
update_cache: true
But I can't limit it to a single package or pattern (like java*).
What I ended up with is ugly and slow (because of the download) workaround:
- name: Check latest available xyz version
yum:
name: xyz
state: latest
download_only: true
become: true
register: _result
- name: Register xyz version
set_fact:
latestXyz: "{{ _result.changes.updated[0][1] | regex_search('xyz-(.+).x86_64.*', '\\1') }}"
Is there any better way to achieve this?
One pretty easy way to achieve this is to run the yum module as you would to update the package, but enforce a dry run on it, with the check_mode option, and register the result of this task.
Inspecting the result, you'll realise that the module will list you the current version of the package and the version it would have been updated to, would it not have been run in dry run.
Given, for example
- yum:
name: expat
state: latest
check_mode: true
register: yum_latest_versions
Will populate the variable yum_latest_versions on a fedora:35 container with:
yum_latest_versions:
ansible_facts:
pkg_mgr: dnf
changed: true
failed: false
msg: 'Check mode: No changes made, but would have if not in check mode'
rc: 0
results:
- 'Installed: expat-2.5.0-1.fc35.x86_64'
- 'Removed: expat-2.4.9-1.fc35.x86_64'
Then to extract, you can indeed use a regex to search in the result:
- debug:
msg: >-
{{
(
yum_latest_versions.results
| map('regex_search', '^Installed: expat\-(.*)\.x86_64$', '\1')
).0.0
}}
Finally yields:
msg: 2.5.0-1.fc37
I am trying to check the existing version of the package and run the install task if the same version is not been installed already.
Below is the code I am trying.
- name: Check for existing mono installation
command: "mono --version"
register: current_mono
ignore_errors: true
- name: Running "make install" for Mono
command: make install
args:
chdir: "{{ mono_install_dir }}"
become: yes
when: "mono_version|string not in current_mono.stdout"
First time this will fail because there won't be a stdout in current_mono var.
How to achieve this while running for the first time?
Since you are using make install you are using shell modules.
vars:
software_version: "1.2.3"
tasks:
First time this will fail ...
This is not absolutely necessary when using the following approach
- name: Check for existing version
shell:
cmd: software --version
warn: false
register: result
changed_when: false
failed_when: false
Please take note that some software packages like Java or Python are reporting his version to stderr.
- name: Show result
debug:
msg: "{{ result.stderr }}"
Now you can run your installer.
- name: Install latest version
shell:
cmd: "echo 'installing ...'"
warn: false
register: result
when: "software_version | string not in result.stderr"
- name: Show result
debug:
msg: "{{ result.stdout | default('was on latest version') }}"
You could test this sample playbook by using java or python as software.
I have two problems with creating playbook that works on linux and on macOS.
I have many steps like this one in my playbooks:
- name: install something
package:
name: [something_1, something_2, ...]
state: present
become: yes
It works nice for apt and yum, but when I tried to run this on macOS, homebrew complains:
Running Homebrew as root is extremely dangerous and no longer supported.
I couldn't find elegant way of fixing that in many places. Copy all tasks and use when clause seems overwhelming to me. Probably I could use become_user variable set to root/local_user dependent on distribution, but that's a lot changes too.
Second problem is with head-only formula (homebrew packages that can be installed only with --head flag). What if something_2 need to be installed with this flag? Again I could copy task and change package module to homebrew but that's a lot boilerplate.
Any help?
If you want a single set of tasks that flexible enough for for multiple Linux
package managers and macOS brew, the choice is either more logic or more
duplication.
These three patterns should help. They still have repetition and
boilerplate code, but that's the territory we're in with Ansible plays
for cross-platform.
Declare become: yes (root) globally for Linux only
Address packages that need platform-specific treatment as-needed with when
This might be --head for brew, or setting up a PPA for apt, etc
Map package name discrepancies with variables
For example: brew install ncurses, apt install libncurses5-dev, and dnf install ncurses-devel are all the same library.
1) Declare become: yes (root) globally for Linux only
For Linux hosts, switching to root for installation is the intended behavior.
For macOS a la Homebrew, installing as root is not good. So, we need become: no (false) when using brew, and become: yes (true) otherwise (for
Linux).
In your example the become directive is nested inside each task ("step"). To
prevent duplication, invoke become at a higher lexical scope, before the tasks
start. The subsequent tasks will then inherit the state of become, which is
set based on a conditional expression.
Unfortunately a variable for become at the root playbook scope will be
undefined and throw an error before the first task is run:
# playbook.yml
- name: Demo
hosts: localhost
connection: local
# This works
become: True
# This doesn't - the variable is undefined
become: "{{ False if ansible_pkg_mgr == 'brew' else True }}"
# Nor does this - also undefined
become: "{{ False if ansible_os_family == 'Darwin' else True }}"
tasks:
# ...
To fix this, we can store the tasks in another file and import them, or
wrap the tasks in a block. Either of these patterns will provide
a chance to declare become with our custom variable value in time for the
tasks to pick it up:
# playbook.yml
---
- name: Demo
hosts: localhost
connection: local
vars:
# This variable gives us a boolean for deciding whether or not to become
# root. It cascades down to any subsequent tasks unless overwritten.
should_be_root: "{{ true if ansible_pkg_mgr != 'brew' else false }}"
# It could also be based on the OS type, but since brew is the main cause
# it's probably better this way.
# should_be_root: "{{ False if ansible_os_family == 'Darwin' else True }}"
tasks:
# Import the tasks from another file, which gives us a chance to pass along
# a `become` context with our variable:
- import_tasks: test_tasks.yml
become: "{{ should_be_root }}"
# Wrapping the tasks in a block will also work:
- block:
- name: ncurses is present
package:
name: [libncurses5-dev, libncursesw5-dev]
state: present
- name: cmatrix is present
package:
name: cmatrix
state: present
become: "{{ should_be_root }}"
Now there is a single logic check for brew and a single before directive
(depending on which task pattern above is used). All tasks will be executed as
the root user, unless the package manager in use is brew.
2) Address packages that need platform-specific treatment as-needed with when
The Package Module is a great convenience but it's quite limited. By
itself it only works for ideal scenarios; meaning, a package that doesn't
require any special treatment or flags from the underlying package manager. All
it can do is pass the literal string of the package to install, the state, and
an optional parameter to force use of a specific package manager executable.
Here's an example that installs wget with a nice short task and only becomes
verbose to handle ffmpeg's special case when installed with brew:
# playbook.yml
# ...
tasks:
# wget is the same among package managers, nothing to see here
- name: wget is present
when: ansible_pkg_mgr != 'brew'
package:
name: wget
state: present
# This will only run on hosts that do not use `brew`, like linux
- name: ffmpeg is present
when: ansible_pkg_mgr != 'brew'
package:
name: ffmpeg
state: present
# This will only run on hosts that use `brew`, i.e. macOS
- name: ffmpeg is present (brew)
when: ansible_pkg_mgr == 'brew'
homebrew:
name: ffmpeg
# head flag
state: head
# --with-chromaprint --with-fdk-aac --with-etc-etc
install_options: with-chromaprint, with-fdk-aac, with-etc-etc
The play above would produce this output for ffmpeg against a Linux box:
TASK [youtube-dl : ffmpeg is present] ******************************************
ok: [localhost]
TASK [youtube-dl : ffmpeg is present (brew)] ***********************************
skipping: [localhost]
3) Map package name discrepancies with variables
This isn't specifically part of your question but it's likely to come up next.
The Package Module docs also mention:
Package names also vary with package manager; this module will not "translate"
them per distro. For example libyaml-dev, libyaml-devel.
So, we're on our own to handle cases where the same software uses different
names between package manager platforms. This is quite common.
There are multiple patterns for this, such as:
Use separate variable files for each OS/distro and import them
conditionally
Use a role with its own variables
Use the same package manager across platforms, such as Homebrew or
Conda
Compile everything from source via git
None of them are very pleasant. Here is an approach using a role. Roles do
involve more boilerplate and directory juggling, but in exchange they provide
modularity and a local variable environment. When a set of tasks in a role
requires more finagling to get right, it doesn't end up polluting other task
sets.
# playbook.yml
---
- name: Demo
hosts: localhost
connection: local
roles:
- cmatrix
# roles/cmatrix/defaults/main.yml
---
ncurses:
default:
- ncurses
# Important: these keys need to exactly match the name of package managers for
# our logic to hold up
apt:
- libncurses5-dev
- libncursesw5-dev
brew:
- pkg-config
- ncurses
# roles/cmatrix/tasks/main.yml
---
- name: cmatix and its dependencies are present
become: "{{ should_be_root }}"
block:
- name: ncurses is present
package:
name: '{{ item }}'
state: latest
loop: "{{ ncurses[ansible_pkg_mgr] | default(ncurses['default']) }}"
- name: cmatrix is present
when: ansible_pkg_mgr != 'brew'
package:
name: cmatrix
state: present
The task for ncurses looks for an array of items to loop through keyed by the
corresponding package manager. If the package manager being used is not defined
in the variable object, a Jinja default filter is employed to reference the
default value we set.
With this pattern, adding support for another package manager or additional
dependencies simply involves updating the variable object:
# roles/cmatrix/defaults/main.yml
---
ncurses:
default:
- ncurses
apt:
- libncurses5-dev
- libncursesw5-dev
# add a new dependency for Debian
- imaginarycurses-dep
brew:
- pkg-config
- ncurses
# add support for Fedora
dnf:
- ncurses-devel
Combining everything into a real play
Here's a full example covering all three aspects. The playbook has two roles
that each use the correct become value based on a single variable. It also
incorporates an special cases for cmatrix and ffmpeg when installed with
brew, and handles alternate names for ncurses between package managers.
# playbook.yml
---
- name: Demo
hosts: localhost
connection: local
vars:
should_be_root: "{{ true if ansible_pkg_mgr != 'brew' else false }}"
roles:
- cmatrix
- youtube-dl
# roles/cmatrix/defaults/main.yml
ncurses:
default:
- ncurses
apt:
- libncurses5-dev
- libncursesw5-dev
brew:
- pkg-config
- ncurses
dnf:
- ncurses-devel
# roles/cmatrix/tasks/main.yml
---
- name: cmatrix and dependencies are present
# A var from above, in the playbook
become: "{{ should_be_root }}"
block:
- name: ncurses is present
package:
name: '{{ item }}'
state: latest
# Get an array of the correct package names to install from the map in our
# default variables file
loop: "{{ ncurses[ansible_pkg_mgr] | default(ncurses['default']) }}"
# Install as usual if this is not a brew system
- name: cmatrix is present
when: ansible_pkg_mgr != 'brew'
package:
name: cmatrix
state: present
# If it is a brew system, use this instead
- name: cmatrix is present (brew)
when: ansible_pkg_mgr == 'brew'
homebrew:
name: cmatrix
state: head
install_options: with-some-option
# roles/youtube-dl/tasks/main.yml
---
- name: youtube-dl and dependencies are present
become: "{{ should_be_root }}"
block:
- name: ffmpeg is present
when: ansible_pkg_mgr != 'brew'
package:
name: ffmpeg
state: latest
- name: ffmpeg is present (brew)
when: ansible_pkg_mgr == 'brew'
homebrew:
name: ffmpeg
state: head
install_options: with-chromaprint, with-fdk-aac, with-etc-etc
- name: atomicparsley is present
package:
name: atomicparsley
state: latest
- name: youtube-dl is present
package:
name: youtube-dl
state: latest
The result for Ubuntu:
$ ansible-playbook demo.yml
[WARNING]: provided hosts list is empty, only localhost is available. Note that
the implicit localhost does not match 'all'
PLAY [Demo] ********************************************************************
TASK [Gathering Facts] *********************************************************
ok: [localhost]
TASK [cmatrix : ncurses is present] ********************************************
ok: [localhost] => (item=libncurses5-dev)
ok: [localhost] => (item=libncursesw5-dev)
TASK [cmatrix : cmatrix is present] ********************************************
ok: [localhost]
TASK [cmatrix : cmatrix is present (brew)] *************************************
skipping: [localhost]
TASK [youtube-dl : ffmpeg is present] ******************************************
ok: [localhost]
TASK [youtube-dl : ffmpeg is present (brew)] ***********************************
skipping: [localhost]
TASK [youtube-dl : atomicparsley is present] ***********************************
ok: [localhost]
TASK [youtube-dl : youtube-dl is present] **************************************
ok: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=6 changed=0 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0
An Ansible role supports Debian Stretch and Buster.
It is not able to do the job on Jessie or older versions.
Which is the best way to tell the user that the role cannot be used on a given old version?
Do nothing in main.yml file (controlling the distro version using when: declarations)
Let the role explicitly fail using the fail module
Do not check for a supported distro version and let tasks fail themselves
Developers should place the supported/tested versions in the Readme. Then users should always read the Readme. Then, common sense should be used.
But we all know that's not the case.
You could configure the host(s) which are too old skip to the role, to ensure the hosts do not execute any command for that role. But the way to go would be to built another role, or update that role, to let that playbook support that OS version.
This method is the least desired one: Do not check for a supported distro version and let tasks fail themselves. Because when you go down this path, then some unsupported tasks are executed on the host and then you can't guarantee the state of the system anymore. In short; you'll create a mess.
To simply prevent the nightmare, indeed, let the play fail:
- name: fail when using older version
fail:
msg: "You fail because reason, woohoo"
when: ansible_distribution is Ubuntu and ansible_distribution_version is 10.04
Q: "What is the best way to manage unsupported distros in an Ansible role?"
A: It's a good idea to end the host or play when the platform and version is not supported. In most cases, this means such a platform and version hasn't been tested yet. It's up to the user to add a new platform and version to the metadata, test it and optionally contribute to the development.
In a role, it's possible to read the variable galaxy_info from the role's file meta/main.yml and test the supported platforms and versions.
$ cat roles/role_1/meta/main.yml
galaxy_info:
author: your name
description: your role description
company: your company (optional)
license: license (GPL-2.0-or-later, MIT, etc)
min_ansible_version: 2.9
platforms:
- name: Ubuntu
versions:
- bionic
- cosmic
- disco
- eoan
galaxy_tags: []
dependencies: []
For example the tasks in the role below
$ cat roles/role_1/tasks/main.yml
---
- name: Print OS and distro Ansible variables collected by setup
debug:
msg:
- "ansible_os_family: {{ ansible_os_family }}"
- "ansible_distribution: {{ ansible_distribution }}"
- "ansible_distribution_major_version: {{ ansible_distribution_major_version }}"
- "ansible_distribution_version: {{ ansible_distribution_version }}"
- "ansible_distribution_release: {{ ansible_distribution_release }}"
- name: Include roles' meta data
include_vars:
file: "{{ role_path }}/meta/main.yml"
- name: Test the distribution is supported. End the host if not.
set_fact:
supported_distributions: "{{ galaxy_info.platforms|json_query('[].name') }}"
- debug:
var: supported_distributions
- block:
- debug:
msg: "{{ ansible_distribution }} not supported. End of host."
- meta: end_host
when: ansible_distribution not in supported_distributions
- name: Test the release is supported. End the host if not.
set_fact:
supported_releases: "{{ (galaxy_info.platforms|
selectattr('name', 'match', ansible_distribution)|
list|first).versions }}"
- debug:
var: supported_releases
- block:
- debug:
msg: "{{ ansible_distribution_release}} not supported. End of host."
- meta: end_host
when: ansible_distribution_release not in supported_releases
- name: The distribution and release is supported. Continue play.
debug:
msg: "{{ ansible_distribution }} {{ ansible_distribution_release }} is supported. Continue play."
with the playbook
- hosts: localhost
gather_facts: true
roles:
- role_1
give
"msg": [
"ansible_os_family: Debian",
"ansible_distribution: Ubuntu",
"ansible_distribution_major_version: 19",
"ansible_distribution_version: 19.04",
"ansible_distribution_release: disco"
]
"supported_distributions": [
"Ubuntu"
]
"supported_releases": [
"bionic",
"cosmic",
"disco",
"eoan"
]
"msg": "Ubuntu disco is supported. Continue play."
I have an Ansible playbook (using ansible 1.9.3) that sets up and updates a bunch of servers. One of those steps is to install packages (using the 'apt' plugin). Until recently all of my servers have been uniform (same version of Ubuntu server). I'm introducing a newer Ubuntu server and some of package names have changed.
Here's what my tasks file looks like:
- name: install needed packages
apt: name={{ packages }} state=present update_cache=yes
(and I have a list of packages in a vars file).
I could define a variable in my inventory file for the hosts with a different Ubuntu version.
How do I change this task to use one list of packages for my current hosts, and another list of packages for my newer hosts?
Use a when clause. Here's an example that will only run on ubuntu 12.04
when: "'{{ ansible_distribution}}' == 'Ubuntu' and '{{ ansible_distribution_release }}' == 'precise'"
You could alternatively use:
when: "'{{ ansible_distribution }}' == 'Ubuntu' and '{{ ansible_distribution_version }}': '12.04'"
Edit:
Later versions of Ansible (e.g. 2.7.1) would print a warning for this statement:
[WARNING]: when statements should not include jinja2 templating delimiters such as {{ }} or {% %}. Found: '{{ansible_distribution}}' == 'Ubuntu' and '{{ ansible_distribution_release }}' == 'bionic'
Ansible now expects another syntax for conditional statements, as stated in the docs.
For Ansible 2.5 you could try this:
when: "ansible_distribution|string == 'Ubuntu' and ansible_distribution_release|string == 'bionic'"
or even:
when:
- ansible_distribution|string == 'Ubuntu'
- ansible_distribution_release|string == 'bionic'
The latest version of the docs suggests to use access the ansible_facts like this:
when:
- ansible_facts['distribution'] == "CentOS"
- ansible_facts['distribution_major_version'] == "6"
To keep your playbooks cleaner, you could do this within the roles, for example, you could have a single tasks/main.yml the one based on the OS it will include the tasks matching the condition used with when, for example, if using ansible_os_family the contents of main.yml could be something like:
---
- include_tasks: freebsd.yml
when: ansible_os_family == "FreeBSD"
tags:
- unbound
- include_tasks: debian.yml
when: ansible_os_family == "Debian"
tags:
- unbound
- include_tasks: redhat.yml
when: ansible_os_family == "RedHat"
tags:
- unbound
The advantage of this approach is that besides only having a set of task per OS, you can also tag the full tasks.
This can be done even simpler and more generic allowing re-use of the same task for multiple distributions or os:es
Simply storing the packages per distribution in a dictionary
vars file:
packages: {
bionic: [ bionic-package1, bionic-package2],
focal: [focal-package1, focal-package2]
}
playbook:
- name: Install unique packages per distro
package:
name: "{{ packages[ansible_distribution_release] }}"
state: present
Note that the change to "package" module from "apt" which allows task to run on any Linux distributions not only ones using apt as a package manager