If you are not familiar with modules, check out Ansible - Getting Started with Modules.
Let's say you have two systems, the Ansible controller (that's the system Ansible is installed and running on) and a target server, and let's say each system has a different version of Python. Perhaps the Python CLI on the Ansible controller is Python version 3.12.0.
[admin@controller ~]$ python --version
Python 3.12.0
And the target server contains both Python version 2.7.5 and 3.9.18.
[john.doe@server1 ~]$ python --version
Python 2.7.5
[john.doe@server1 ~]$ python3 --version
Python 3.9.18
Let's say you have the following playbook, which uses the ansible.builtin.pip module to install the "requests" module in Python on the target server.
You may need to include become: yes and become_user: root since this probably requires sudo or root.
---
- hosts: all
tasks:
- name: use pip to install the 'requests' module in Python
become: yes
become_user: root
ansible.builtin.pip:
name: requests
state: latest
...
Let's run the ansible-playbook command with the -vvv (very verbose) flag.
ansible-playbook example.yml --inventory server1.example.com, --user john.doe --ask-pass -vvv
So, underneath the hood, a directory is created on the target server, something like this.
/home/john.doe/.ansible/tmp/ansible-tmp-1713408593.1331322-16539-164674748277391
This can been seen in the output with the -vvv flag. The output should have something like this.
<server1.example.com> SSH: EXEC \
sshpass -d12 \
ssh -C \
-o ControlMaster=auto \
-o ControlPersist=60s \
-o 'User="john.doe"' \
-o ConnectTimeout=10 \
-o 'ControlPath="/home/john.doe/.ansible/cp/da9b18fba3"' \
server1.example.com \
'/bin/sh -c '"'"'( umask 77 && mkdir -p "` echo /home/john.doe/.ansible/tmp `"&& mkdir "` echo /home/john.doe/.ansible/tmp/ansible-tmp-1713408596.274309-16590-237914960550324 `" && echo ansible-tmp-1713408596.274309-16590-237914960550324="` echo /home/john.doe/.ansible/tmp/ansible-tmp-1713408596.274309-16590-237914960550324 `" ) && sleep 0'"'"''
It is important to recognize that the directory will be removed, so for this excercise, I would set the ANSIBLE_KEEP_REMOTE_FILES environment variable to 1 which instructs the Ansible controller to not delete the tmp files and directories that get created on the target server.
export ANSIBLE_KEEP_REMOTE_FILES=1
And then the AnsiballZ_file.py Python script is PUT from the Ansible controller to the target server.
<server1.example.com> PUT /home/admin/.ansible/tmp/ansible-local-1681708tty8e7/tmpclmqm36v TO /home/john.doe/.ansible/tmp/ansible-tmp-1713405726.6487887-17223-276200849623319/AnsiballZ_pip.py
Ansible uses the sshpass command to connect to the target server and then ssh command and the /bin/sh (shell) CLI with the -c (command) flag to run a command on the target server. The command that is run on the target server is almost always /home/john.doe/.ansible/tmp/ansible-tmp/AnsiballZ_<module name>.py, a Python script. Since this command is being run on the target server, this uses the Python CLI on the target server, not Python on the Ansible controller.
<server1.example.com> SSH: EXEC \
sshpass -d12 \
ssh -C \
-o ControlMaster=auto \
-o ControlPersist=60s \
-o 'User="john.doe"' \
-o ConnectTimeout=10 \
-o 'ControlPath="/home/john.doe/.ansible/cp/da9b18fba3"' \
server1.example.com \
'/bin/sh -c '"'"'chmod u+x /home/john.doe/.ansible/tmp/ansible-tmp-1713405726.6487887-17223-276200849623319/ /home/john.doe/.ansible/tmp/ansible-tmp-1713405726.6487887-17223-276200849623319/AnsiballZ_pip.py && sleep 0'"'"''
And if we look at line 1 in AnsiballZ_pip.py, we can see it is using /usr/bin/python (in this example).
[john.doe@server1 ~]$ head -1 /home/webproc/.ansible/tmp/ansible-tmp-1713408594.9606252-16563-253322922038816/AnsiballZ_file.py
#!/usr/bin/python
Which means that the "requests" module would be installed in /usr/bin/python which is almost always a symbolic link to a certain version of Python. In this example, /usr/bin/python is symbolically linked to /usr/bin/python2.7, meaning that the actual version of Python being used here is Python verison 2.7.
[john.doe@server1 ~]$ ll /usr/bin/ | grep -i python
lrwxrwxrwx. 1 root root 9 Jan 5 07:55 /usr/bin/python -> python2.7
lrwxrwxrwx. 1 root root 9 Jan 5 07:55 /usr/bin/python3 -> python3.9
One option is to include the executable parameter in the ansible.builtin.pip module to use pip (for Python 2.7 in this example) or pip3 (for Python 3.9 in this example).
---
- hosts: all
tasks:
- name: use pip3 to install the 'requests' module in Python3
ansible.builtin.pip:
name: requests
state: latest
executable: pip3
...
Or, there are a few different ways to tell Ansible to run the AnsiballZ_<module>.py files on the target servers with a certain version of Python.
- Using the ansible_python_interpreter variable
- Using the interpreter_python directive in ansible.cfg
Let's say the playbook includes ansible_python_interpreter: /usr/bin/python3.9.
---
- hosts: all
vars:
ansible_python_interpreter: /usr/bin/python3.9
tasks:
- ansible.builtin.file:
path: /tmp/foo.txt
state: "{{ item }}"
with_items:
- touch
- absent
...
This will cause the AnsiballZ_file.py to have /usr/bin/python3.9.
[john.doe@server1 ~]$ head /home/john.doe/.ansible/tmp/ansible-tmp-1713410268.6565683-30050-40883123872239/AnsiballZ_file.py
#!/usr/bin/python3.9
Here is how you could install a specific version of the "requests" module.
---
- hosts: all
tasks:
- name: use pip to install version 2.25.1 of the 'requests' module in Python
ansible.builtin.pip:
name: requests=2.25.1
...
To install two (or more) packages.
---
- hosts: all
tasks:
- name: pip install the latest versions of requests and django
ansible.builtin.pip:
name:
- requests
- django
state: latest
...
To uninstall pymysql.
---
- hosts: all
tasks:
- name: pip uninstall pymysql
ansible.builtin.pip:
name: pymysql
state: absent
...
Did you find this article helpful?
If so, consider buying me a coffee over at