Ansible playbook to install different packages depending on os version - ansible

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

Related

Ansible: Playbook to install packages using yum and dnf

I'm having problem executing an Ansible playbook to install a package using yum on RHEL7 and dnf on RHEL8.
I'm using a condition as shown on my playbook below, but keep getting errors.
Error
{"msg": "The conditional check 'ansible_os_family == \"RedHat\" and ansible_lsb.major_release|int == \"7\"' failed. The error was: error while evaluating conditional (ansible_os_family == \"RedHat\" and ansible_lsb.major_release|int == \"7\"): 'dict object' has no attribute 'major_release'\n\nThe error appears to be in '/ansible/master/intall.pkg.yml': line 9, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n # (Task-1) Checks if ansible_os_family == \"RHEL7\" and then Installs telnet on Remote Node\n - name: Install telnet on RHEL7 Server\n ^ here\n"}
Playbook
---
- hosts: all
gather_facts: true
become: yes
#become_user: ansible
become_method: sudo
tasks:
# (Task-1) Checks if ansible_os_family == "RHEL7" and then Installs telnet on Remote Node
- name: Install telnet on RHEL7 Server
yum: name=telnet state=present
when: ansible_os_family == "RedHat" and ansible_lsb.major_release|int == "7"
# (Task-2) Checks if ansible_os_family == "RHEL8" and then Installs telnet on Remote Node
- name: Install telnet on RHEL8 Server
package: name=telnet state=present
when: ansible_os_family == "RedHat" and ansible_lsb.major_release|int == "8"
How do I use my playbook to skip the RHEL7 and install the package on RHEL8 using dnf?
Thank you.
As already mentioned within the comments by #Zeitounator you may have a look into Conditionals based on ansible_facts since the fact respective variable is a string.
The following minimal example is working in an production environment.
- name: "Install telnet on RHEL-{{ ansible_distribution_major_version }} Remote Node"
yum:
name: telnet
state: present
when: ansible_distribution == 'RedHat' and ansible_distribution_major_version == '7'
- name: "Install telnet on RHEL-{{ ansible_distribution_major_version }} Remote Node"
yum:
name: telnet
state: present
when: ansible_distribution == 'RedHat' and ansible_distribution_major_version == '8'
This is possible because of Software management tools in RHEL 8
Although YUM v4 used in RHEL 8 is based on DNF, it is compatible with YUM v3 used in RHEL 7. For software installation, the yum command and most of its options work the same way in RHEL 8 as they did in RHEL 7.
This means that in your specific case by using the yum or package module
This modules manages packages on a target without specifying a package manager module (like ansible.builtin.yum, ansible.builtin.apt, …). It is convenient to use in an heterogeneous environment of machines without having to create a specific task for each package manager. package calls behind the module for the package manager used by the operating system discovered by the module ansible.builtin.setup.
there would be no need for a conditional check unless there would be differences in the availability of packages and or naming.
A single task like
- name: "Install telnet on RHEL-{{ ansible_distribution_major_version }} Remote Node"
package: # or even yum
name: telnet
state: present
should just work.
Depending on your infrastructure and to be prepared for future releases you could switch to the dnf module for RHEL-8 and RHEL-9 instances.
- name: "Install telnet on RHEL-{{ ansible_distribution_major_version }} Remote Node"
dnf:
name: telnet
state: present
when: ansible_distribution == 'RedHat' and ansible_distribution_major_version == '8'

How to print package names that are being install with a loop in Ansible.?

I would like to print the package names each time ansible do a loop
- name: Install base packages
package:
name: "{{ packages }}"
state: present
vars:
packages:
- git
- vim
- htop
register: echo
debug: "{{packages}}"
when: ansible_pkg_mgr == 'apt'
Actually your task definition is not looping through the packages. The name parameter can take a list of packages (which is preferred), and that is what you are passing as packages. If you would like to loop and have each package installed iteratively, you should have a loop with loop: {{ packages }}.
Something like below:
- name: Install base packages
package:
name: "{{ item }}"
state: present
loop: "{{ packages }}"
vars:
packages:
- git
- vim
- htop
when: ansible_pkg_mgr == 'apt'
Now, every time the task "loops", the name of the item, i.e. the package name (e.g. item=git) will be shown.
The code can be written more efficiently. I've added the debug part so you can see the actual output which happened on the target system.
- name: Install base packages
apt:
pkg:
- git
- vim
- htop
register: install_pkgs
when: ansible_pkg_mgr == 'apt'
- debug:
msg: "{{ install_pkgs }}"

How to stop executing roles if the OS versions are different with in inventory

I have two different Centos versions 6.x and 7.x in my Inventory. I am able to print OS versions using below code
- name: Get OS details
debug: msg="{{ item }}"
with_items:
- "{{ ansible_distribution_version }}"
How to stop executing remaining roles if OS version is 7 instead of 6 or vice versa? I have all my inventory in one file.
If i'm running the code against version 6 then it needs to print the hostnames if it's not version 6 and stop executing remaining roles.
You can use the meta option, end_play, for example
- name: end play if not centos6
meta: end_play
when: ansible_distribution_version == "centos6"
More information in https://docs.ansible.com/ansible/latest/modules/meta_module.html
what you probably want to do is split your setup tasks into tasks/centos6.yml and tasks/centos7.yml and only include the relevant file in your playbook, for example
- name: setup centos7
include: tasks/centos7.yml
when: ansible_distribution_version == centos7

What is the best way to manage unsupported distros in an Ansible role?

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."

Abort if none of previous tasks were run

I have a tasks.yml that contains something like :
- name: something on Debian wheezy
when: ansible_distribution == 'Debian' and ansible_distribution_version == '7'
- name: something on Debian jessie
when: ansible_distribution == 'Debian' and ansible_distribution_version == '8'
- name: something on Ubuntu
when: ansible_distribution == 'Ubuntu'
I want to add a task (or something) that aborts the playbook if none of those tasks were run.
Maybe "something" is installing a package, and some tasks that come after are configuring that newly installed package. If I run the playbook on a RedHat system, that package won't be installed, and I want to abort before executing the tasks that configure it.
I think I could do something like:
- fail: msg="abort!"
when: not (ansible_distribution == 'Debian' and ansible_distribution_version == '7') and not (ansible_distribution == 'Debian' and ansible_distribution_version == '8') and not ansible_distribution == 'Ubuntu'
... but that's annoying. Is there any better solution?
---
- name: test
hosts: localhost
tasks:
- name: Install pkg
debug: msg="Installed pkg"
when: "{{ ansible_distribution == 'Debian' }}"
register: debian_pkg
- name: Install pkg
debug: msg="Installed pkg"
when: "{{ ansible_distribution == 'Ubuntu' }}"
register: ubuntu_pkg
- fail: msg="Unsupported OS"
when: "debian_pkg|skipped and ubuntu_pkg|skipped"
I finally went a simpler, more "business"-accurate way. I want to install something, and I then expect something to be installed. In fact, in my infrastructure where not everything is automated (I'm not root everywhere) I sometimes can't install something, but I still want to check that it is installed (it was installed out-of-band).
So, after the tasks that install something depending on the OS, I added a task that checks if it is installed, simply by trying to run it. Something like that:
- include: install_something.yml # contains the install tasks for each OS
when: can_become_root
- name: Check that something is installed
command: something --version

Resources