Table of Contents
Go from Ansible beginner to Ansible pro with this full video course.
If you try to run Ansible against some fresh Ubuntu 18.04 instances you’ll very likely run into an error like this:
TASK [Gathering Facts]
fatal: [123.123.123.123]: FAILED! => {"changed": false,
"module_stderr": "/bin/sh: 1: /usr/bin/python: not found\n",
"module_stdout": "", "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
"rc": 127
This is because Ubuntu 18.04 does not include /usr/bin/python
(Python 2) by default. You can confirm this for yourself by ssh
ing into the instance and checking for python
:
$ which python
$ python --version
Command 'python' not found, but can be installed with:
sudo apt install python3
sudo apt install python
sudo apt install python-minimal
You also have python3 installed, you can run 'python3' instead.
The shell helpfully points out that we have python3
installed and can use that instead. The reason our Ansible failed even though python3
is installed is because Ansible still tries to use Python 2 (/usr/bin/python
) by default.
The available solutions
There are 3 ways to solve this problem if you encounter it on your remote host:
- Set
ansible_python_interpreter: /usr/bin/python3
variable for all hosts that havepython3
installed by default - Install Python 2 using Ansible’s
raw
module - Symlink
/usr/bin/python3
to/usr/bin/python
using Ansible’sraw
module
All 3 options can be done in Ansible, without ssh
ing into the host, which is good news for us (and automation). In the following sections I’ll show how you can use each option and the pros/cons of each.
Option 1 - Set ansible_python_interpreter: /usr/bin/python3
for hosts that have python3
installed by default
This is the option put forward in the official Ansible docs on Python 3 support. The docs suggest to set ansible_python_interpreter: /usr/bin/python3
for remote hosts that have Python 3 installed. Let’s test it out shall we?
Take a look at the result of running the following playbook against a fresh Ubuntu 18.04 instance:
- name: misc task on ubuntu 18.04 instance
hosts: "*"
tasks:
- debug: var=ansible_host
We don’t even make it past the Gathering Facts
task:
PLAY [misc task on ubuntu 18.04 instance]
TASK [Gathering Facts]
fatal: [123.123.123.123]: FAILED! => {"changed": false,
"module_stderr": "/bin/sh: 1: /usr/bin/python: not found\n",
"module_stdout": "", "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
"rc": 127
Now let’s try again with ansible_python_interpreter: /usr/bin/python3
:
- name: misc task on ubuntu 18.04 instance
hosts: "*"
vars:
ansible_python_interpreter: /usr/bin/python3
tasks:
- debug: var=ansible_host
No errors this time:
PLAY [misc task on ubuntu 18.04 instance]
TASK [Gathering Facts]
ok: [123.123.123.123]
TASK [debug]
ok: [123.123.123.123] => {
"ansible_host": "123.123.123.123"
}
We can include this variable in a few ways:
- If all your remote hosts definitely have
python3
, you can just add the variable to yourgroup_vars/all.yml
file:
# group_vars/all.yml
ansible_python_interpreter: /usr/bin/python3
- If you’re using dynamic inventory and some of your hosts don’t have
python3
installed by default you could add tags that are read by your inventory script (e.g.ec2.py
) and then apply theansible_python_interpreter
in agroup_vars
file based on the tag:
# group_vars/tag_OS_ubuntu1804.yml
ansible_python_interpreter: /usr/bin/python3
- If you’re using static inventory and some of your hosts don’t have
python3
installed by default you can add variables to groups in yourinventory/hosts
file:
[python2_hosts]
centos7_server
[python3_hosts]
u1804_server
[python3_hosts:vars]
ansible_python_interpreter=/usr/bin/python3
Pros
The biggest advantage of this approach is that no changes are required on the remote hosts themselves, just a relatively minor change on the local host (and any tagging required).
Cons
The massive disadvantage of this approach is that you need to know in advance of running the playbook that the remote host definitely has Python 3 installed. If Python 3 isn’t available on the remote host, setting ansible_python_interpreter: /usr/bin/python3
will make your plays fail.
Setting tags or grouping your hosts by whether or not they have Python 3 could be a massive pain if you manage a lot of infrastructure.
Option 2 - Install Python 2 on a remote host using Ansible’s raw
module
This solution entails creating a separate play with gather_facts: false
and then use Ansible’s raw
module to run commands on the remote host, bypassing all Ansible modules.
gather_facts: false
will prevent Ansible from running the Gathering Facts
task, which runs the setup
module against the host.
Use of raw
is highly discouraged and it should only really be used for special cases like this. Note that the raw
module has no “change checking” – it will run every single time and is not idempotent unless the commands you’re running with it are themselves idempotent.
The commands we want to run with raw
are what we would run on the host to get Python 2 installed:
sudo apt-get update
sudo apt-get -y install python
Let’s convert them into commands for raw
in our new play:
- name: install python2 on ubuntu 18.04 instances
hosts: "*"
gather_facts: false
tasks:
- name: run apt-get update and install python
raw: "{{ item }}"
loop:
- sudo apt-get update
- sudo apt-get -y install python
become: true
- name: misc task on ubuntu 18.04 instance
hosts: "*"
tasks:
- debug: var=ansible_host
The plays above should run successfully and give you output similar to this:
PLAY [install python2 on ubuntu 18.04 instances]
TASK [run apt-get update and install python]
changed: [123.123.123.123] => (item=sudo apt-get update)
changed: [123.123.123.123] => (item=sudo apt-get -y install python)
PLAY [misc task on ubuntu 18.04 instance]
TASK [Gathering Facts]
ok: [123.123.123.123]
TASK [debug]
ok: [123.123.123.123] => {
"ansible_host": "123.123.123.123"
}
Notice the absence of the Gathering Facts
task on the first play. The tasks in the second play succeed because Python 2 is now available.
Pros
Running this play at the top of a playbook means that we don’t have to set ansible_python_interpreter: /usr/bin/python3
for any of our remote hosts. We can adapt the play so that we can run it against all hosts and ignore failures if they don’t have apt-get
by adding ignore_errors: true
:
- name: install python2 on all instances
hosts: "*"
gather_facts: false
tasks:
- name: run apt-get update and install python
raw: "{{ item }}"
loop:
- sudo apt-get update
- sudo apt-get -y install python
become: true
ignore_errors: true
Cons
We need to add this extra play to the top of every playbook (or include
it). This is a lot of needless repetition and adds a potential source of failure if we haven’t done it.
Since there is no change checking, it also means we need to run the play fully every time we run the playbook, which adds unnecessary extra time to run our playbooks.
Option 3 - Symlink /usr/bin/python -> /usr/bin/python3
using Ansible’s raw
module
Another option in a similar vein to option 2 is to use the raw
module to “symlink” /usr/bin/python -> /usr/bin/python3
.
With a bit of shell magic, we can fashion a command to do this conditionally based on whether either of the files exist using conditionals:
if [ -f /usr/bin/python3 ] && [ ! -f /usr/bin/python ]; then
ln --symbolic /usr/bin/python3 /usr/bin/python;
fi
This command says:
If
/usr/bin/python3
exists and/usr/bin/python
does not exist then create a symbolic link/usr/bin/python -> /usr/bin/python3
.
This makes the command flexible enough to run on all systems without any ignore_errors: true
. For example, on a CentOS 7 system, the conditional will evaluate to false
because /usr/bin/python3
does not exist.
In Ansible form:
- name: symlink /usr/bin/python -> /usr/bin/python3
hosts: "*"
gather_facts: false
tasks:
- name: symlink /usr/bin/python -> /usr/bin/python3
raw: |
if [ -f /usr/bin/python3 ] && [ ! -f /usr/bin/python ]; then
ln --symbolic /usr/bin/python3 /usr/bin/python;
fi
become: true
- name: misc task on all instances
hosts: "*"
tasks:
- debug: var=ansible_host
You should see the following output:
PLAY [symlink /usr/bin/python -> /usr/bin/python3]
TASK [symlink /usr/bin/python -> /usr/bin/python3]
changed: [123.123.123.123]
PLAY [misc task on all instances]
TASK [Gathering Facts]
ok: [123.123.123.123]
TASK [debug]
ok: [123.123.123.123] => {
"ansible_host": "123.123.123.123"
}
Pros
Compared to option 2, this method is significantly faster and doesn’t require us to ignore errors. Also, using a conditional statement means that we create the symlink only if python3
exists and python
doesn’t, which:
- prevents errors in the case
python3
doesn’t exist - prevents us from overriding
python
if it’s already installed
Cons
The only real downside of this method is that – like option 2 – we need to add or include
this play at the top of all our playbooks, which adds unnecessary duplication and may be a source of errors.
Conclusion and my recommendations
This is a really difficult one to call. Each option has advantages and disadvantages and the solution I recommend really depends on your situation.
To me, option 1 is generally superior because it doesn’t require changing your playbooks and doesn’t require changing the remote host in any way.
I think option 1 is the best solution if all your remote hosts are running a distro with Python 3 installed by default (e.g. Ubuntu 16.04 and up). It’s also the best solution if you’ve already tagged your infrastructure with the OS and OS version (e.g. AWS tags), or if you’re using static infrastructure and you’ve already grouped your servers by OS.
I think option 3 is the best solution if you are dealing with remote hosts that may not have Python 3 installed by default, or have no easy way to apply group_vars
based on the OS. The reason this option wins out over option 2 is because it’s significantly faster and can run against any distro without any special error handling.
Know of any other ways to solve this problem? Please comment below and let me know what you think!
Further reading
- Python 3 Support on Ansible Docs
- Ansible
raw
Module on Ansible Docs - Ansible
setup
Module on Ansible Docs - Error handling in playbooks on Ansible Docs