Infiniroot Blog: We sometimes write, too.

Of course we cannot always share details about our work with customers, but nevertheless it is nice to show our technical achievements and share some solutions.

Linux distribution and release discovery in Ansible - and how to handle no ansible_distribution for Ubuntu 20.04

Published on August 28th 2020 - see original post


A lot of Ansible playbooks are adapted to multiple Linux distributions. Depending on the distribution and even between the different releases, a lot of changes could have happened. Typical examples: Introduction of Systemd or netplan, different configuration paths, etc.

How to detect distributions and releases

Whenever Ansible (server) connects to a client in the inventory, it looks up information about the client and stores them in variables which can be accessed during runtime (typically during a playbook). To see these variables, once can launch the "setup" command and Ansible will output all the variables of that particular client:

ansible@ansibleserver:~$ ansible template2004 -m setup
template2004 | SUCCESS => {
    "ansible_facts": {
        "ansible_all_ipv4_addresses": [
            "10.161.206.48"
        ],
        "ansible_all_ipv6_addresses": [
            "fe80::250:56ff:fe8d:2d82"
        ],
        "ansible_apparmor": {
            "status": "enabled"
        },
        "ansible_architecture": "x86_64",
        "ansible_bios_date": "12/12/2018",
        "ansible_bios_version": "6.00",
        "ansible_cmdline": {
            "BOOT_IMAGE": "/boot/vmlinuz-5.4.0-42-generic",
            "ro": true,
            "root": "UUID=db2cf4f6-7e7c-448b-a333-1caec4e68f45"
        },
[...]

This output can be pretty big. In general: The more client-specific info there is, the more accurate playbooks can be written. Meaning: That's a good thing!

A couple of variables are specifically looking for the client's OS, distribution and version:

ansible@ansibleserver:~$ ansible ubuntu1604 -m setup | grep distribution
        "ansible_distribution": "Ubuntu",
        "ansible_distribution_file_parsed": true,
        "ansible_distribution_file_path": "/etc/os-release",
        "ansible_distribution_file_variety": "Debian",
        "ansible_distribution_major_version": "16",
        "ansible_distribution_release": "xenial",
        "ansible_distribution_version": "16.04",

Based on the variables ansible_distribution and ansible_distribution_relase (or ansible_distribution_version), the playbooks can be adjusted to respect particular configurations for these versions.

Give me some examples

The following tasks as part of a playbook show how variables can be used within a playbook and how to make use of them.

Different network config

You've surely come across the different Linux distributions. In this case you also know that network configurations are different, depending on the distribution. The following task sets DNS servers in the network configuration of the default IPv4 interface (also detected by Ansible) in /etc/sysconfig/network-scripts/ifcfg-*. But as this path only exists on Red Hat (and derivates) and SuSE Linux, it does not make sense to apply this task to other systems.

  - name: DNS - Set DNS servers in /etc/sysconfig/network-scripts/ifcfg-*
    lineinfile: dest=/etc/sysconfig/network-scripts/ifcfg-{{ ansible_default_ipv4.interface }} state=present regexp={{ item.regexp }} line={{ item.line }}
    with_items:
      - { regexp: "DNS1=", line: "DNS1=1.1.1.1" }
      - { regexp: "DNS2=", line: "DNS2=8.8.8.8" }
      - { regexp: "DNS3=", line: "DNS3=8.8.4.4" }
    when: ansible_distribution == "CentOS" or ansible_distribution == "RedHat" or ansible_distribution == "OracleLinux"

The when condition makes sure that this task is only applied on either CentOS, RedHat or OracleLinux systems.

Include a specific sub-playbook

Putting all the tasks and exceptions into one playbook has one big disadvantage: Slowly but surely the playbook loses its visibility. There are too many tasks and exceptions defined in it and one little mistake has an effect on the whole playbook.

A playbook can include a sub-playbook, specifically trimmed for the target client. The following example shows an include of a sub-playbook specifically taking care of certain Ubuntu 18.04 configurations:

  - name: DNS - Include sub-playbook DNS for Ubuntu 18.04
    include: ubuntu1804-dns.yaml
    when: ansible_distribution == "Ubuntu" and ansible_distribution_version == "18.04"

Once again, when makes sure, this include only happens when the distribution was detected as Ubuntu and the distribution version matches 18.04.

Ubuntu 20.04: ansible_distribution gone (?)

With every new distribution release, all the playbooks should be fully tested. It is very likely that something will fail. Either because the new distribution has a new way of doing things (for example replacing /etc/interfaces with netplan) or the existing when conditions in the playbook do not cover the new release.

An interesting case happened when Ubuntu 20.04 was released (in April 2020). Ansible playbooks making use of ansible_distribution suddenly stopped working:

 TASK [DNS - Set DNS servers in /etc/sysconfig/network-scripts/ifcfg-*] **************************************************
fatal: [template2004]: FAILED! => {"msg": "The conditional check 'ansible_distribution == \"CentOS\"  or ansible_distribution == \"RedHat\" or ansible_distribution == \"OracleLinux\"' failed. The error was: error while evaluating conditional  (ansible_distribution == \"CentOS\" or ansible_distribution == \"RedHat\" or ansible_distribution == \"OracleLinux\"): 'ansible_distribution' is undefined\n\nThe error appears to have been in '/srv/ansible/playbooks/allgemein/server-setup.yaml': line  60, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n  - name: DN S - Set DNS servers in /etc/sysconfig/network-scripts/ifcfg-* (internal networks)\n    ^ here\n"}

From the example above we can see that this particular task should never have been applied to this Ubuntu 20.04 client, it should have been skipped. Why did this task cause an error and let the playbook fail though?

The relevant part of the error message is marked bold above and reads in short: error while evaluating conditional: 'ansible_distribution' is undefined.

But as explained in the first part, ansible_distribution is a key variable created by Ansible itself. Why would it be gone? This can be verified by running the setup command again on that particular client:

ansible@ansibleserver:~$ ansible template2004 -m setup | grep -i distribution
ansible@ansibleserver:~$

You have seen it right: No output! By checking the full setup output, only the following variables nested under ansible_lsb could be found to determine the distribution and release:

         "ansible_lsb": {
            "codename": "focal",
            "description": "Ubuntu 20.04.1 LTS",
            "id": "Ubuntu",
            "major_release": "20",
            "release": "20.04"
        },

Does this mean that all the playbooks need to be rewritten to cover both ansible_distribution and ansible_lsb_id variables?

Fortunately there's another solution.

Ansible update to the rescue!

The Ansible version on this particular server was still running on an older version, namely 2.7.x. Updates are available:

ansible@ansibleserver:~# apt-show-versions -u|grep ansible
ansible:all/bionic 2.7.6-1ppa~trusty upgradeable to 2.9.12-1ppa~bionic

Right after the upgrade to 2.9.12, the ansible_distribution variable worked on Ubuntu 20.04, too:

ansible@ansibleserver:~$ ansible template2004 -m setup  | grep -i distribution
[DEPRECATION WARNING]: Distribution ubuntu 20.04 on host template2004 should use /usr/bin/python3, but is using /usr/bin/python
for backward compatibility with prior Ansible releases. A future Ansible release will default to using the discovered platform python for
this host. See https://docs.ansible.com/ansible/2.9/reference_appendices/interpreter_discovery.html for more information. This feature will
be removed in version 2.12. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.
        "ansible_distribution": "Ubuntu",
        "ansible_distribution_file_parsed": true,
        "ansible_distribution_file_path": "/etc/os-release",
        "ansible_distribution_file_variety": "Debian",
        "ansible_distribution_major_version": "20",
        "ansible_distribution_release": "focal",
        "ansible_distribution_version": "20.04",