How to fix the /usr/bin/python: not found error in Ansible


Percy Grunwald's Profile Picture

Written by Percy Grunwald

— Last Updated February 22, 2019

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 sshing 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:

  1. Set ansible_python_interpreter: /usr/bin/python3 variable for all hosts that have python3 installed by default
  2. Install Python 2 using Ansible’s raw module
  3. Symlink /usr/bin/python3 to /usr/bin/python using Ansible’s raw module

All 3 options can be done in Ansible, without sshing 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 your group_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 the ansible_python_interpreter in a group_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 your inventory/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.

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

Comments