
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.
[john.doe@controller ~]$ python --version
Python 3.12.0
And the Python version on a target server is Python version 3.9.18.
[jane.doe@server1 ~]$ python --version
Python 3.9.18
To differentiate between the system that Ansible is running on and the target server, let's say you are using the command line on the system Ansible is running on as john.doe and you are making an SSH connection from the system Ansible is running on to the target system as jane.doe.
Let's say you have the following playbook which uses ansible.builtin.file to create and then delete the /tmp/foo.txt file on the target system and also runs the ls (list) command using ansible.builtin.shell and delegate_to: localhost so that the ls command is run on localhost, which is the system Ansible is running on.
It is also important to notice that gather_facts: false was NOT used, which means that facts will be gathered. More on this in a moment . . . read on.
---
- hosts: all
tasks:
- name: create and then remove /tmp/foo.txt on target server
ansible.builtin.file:
path: /tmp/foo.txt
state: "{{ item }}"
with_items:
- touch
- absent
- name: run the ls command on localhost
ansible.builtin.shell: ls
delegate_to: localhost
...
It is important to recognize that the temporary directories will be removed so for this exercise I would set the ANSIBLE_KEEP_REMOTE_FILES environment variable to 1 which instructs Ansible to not delete the tmp files and directories.
export ANSIBLE_KEEP_REMOTE_FILES=1
Let's run the ansible-playbook command with the -vvv (very very verbose) flag.
[john.doe@controller ~]$ ansible-playbook example.yml --inventory server1.example.com, --user jane.doe --ask-pass -vvv
I've limited the following output to only the most relevant output.
- Since gather_facts: false was NOT used, this means facts will be gathered. In this scenario, the sshpass command and ssh command will be used to make an SSH connection from the system Ansible is running on to target system and then the /bin/sh (shell) CLI with the -c (command) flag will be used to run one or more commands on the target server. In this example, john.doe on the system Ansible is running on makes an SSH connection to server1.example.com as jane.doe and then the mkdir command is used to create the /home/jane.doe/.ansible/tmp/ansible-tmp-1756288819.4388542-2481307-133499454356555 directory on the target server. Then, a PUT request is issued to copy the /home/john.doe/.ansible/tmp/ansible-local-2480994j_ia44_5/tmpaf6qg1dh file from the system Ansible is running on to /home/jane.doe/.ansible/tmp/ansible-tmp-1756288819.4388542-2481307-133499454356555/AnsiballZ_setup.py on the target server.
- Next the "create and then remove /tmp/foo.txt on target server" is run, which created another temporary directory on the target system (/home/jane.doe/.ansible/tmp/ansible-tmp-1756288823.0855155-2481766-234863562006181 in this example) and a PUT request copies /home/john.doe/.ansible/tmp/ansible-local-2480994j_ia44_5/tmp9qtrs5zv from the system Ansible is running on to /home/jane.doe/.ansible/tmp/ansible-tmp-1756288823.0855155-2481766-234863562006181/AnsiballZ_file.py on the target server.
- Last but not least, the "run the ls command on localhost" task is run on localhost (the system Ansible is running on) since delegate_to: localhost was used. In this scenario, temporary directory /home/john.doe/.ansible/tmp/ansible-tmp-1756284934.1524658-2323201-200739628244347 is created on localhost (the system Ansible is running on) and /home/john.doe/.ansible/tmp/ansible-local-2323086l5wencsk/tmpqeqqnev3 is copied to /home/john.doe/.ansible/tmp/ansible-tmp-1756284934.1524658-2323201-200739628244347/AnsiballZ_command.py on localhost.
TASK [Gathering Facts]
<server1.example.com> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o 'IdentityFile="/home/john.doe/.ssh/id_rsa"' -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o 'User="jane.doe"' -o ConnectTimeout=10 -o 'ControlPath="/home/john.doe/.ansible/cp/ce2a111206"' -o NumberOfPasswordPrompts=1 server1 '/bin/sh -c '"'"'( umask 77 && mkdir -p "` echo /home/jane.doe/.ansible/tmp `"&& mkdir "` echo /home/jane.doe/.ansible/tmp/ansible-tmp-1756288819.4388542-2481307-133499454356555 `" && echo ansible-tmp-1756288819.4388542-2481307-133499454356555="` echo /home/jane.doe/.ansible/tmp/ansible-tmp-1756288819.4388542-2481307-133499454356555 `" ) && sleep 0'"'"''
<server1.example.com> PUT /home/john.doe/.ansible/tmp/ansible-local-2480994j_ia44_5/tmpaf6qg1dh TO /home/jane.doe/.ansible/tmp/ansible-tmp-1756288819.4388542-2481307-133499454356555/AnsiballZ_setup.py
TASK [create and then remove /tmp/foo.txt on target server]
<server1.example.com> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o 'IdentityFile="/home/john.doe/.ssh/id_rsa"' -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o 'User="jane.doe"' -o ConnectTimeout=10 -o 'ControlPath="/home/john.doe/.ansible/cp/ce2a111206"' -o NumberOfPasswordPrompts=1 server1 '/bin/sh -c '"'"'( umask 77 && mkdir -p "` echo /home/jane.doe/.ansible/tmp `"&& mkdir "` echo /home/jane.doe/.ansible/tmp/ansible-tmp-1756288823.0855155-2481766-234863562006181 `" && echo ansible-tmp-1756288823.0855155-2481766-234863562006181="` echo /home/jane.doe/.ansible/tmp/ansible-tmp-1756288823.0855155-2481766-234863562006181 `" ) && sleep 0'"'"''
<server1.example.com> PUT /home/john.doe/.ansible/tmp/ansible-local-2480994j_ia44_5/tmp9qtrs5zv TO /home/jane.doe/.ansible/tmp/ansible-tmp-1756288823.0855155-2481766-234863562006181/AnsiballZ_file.py
TASK [run the ls command on localhost]
<localhost> ESTABLISH LOCAL CONNECTION FOR USER: john.doe
<localhost> EXEC /bin/sh -c 'echo ~john.doe && sleep 0'
<localhost> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/john.doe/.ansible/tmp `"&& mkdir "` echo /home/john.doe/.ansible/tmp/ansible-tmp-1756284934.1524658-2323201-200739628244347 `" && echo ansible-tmp-1756284934.1524658-2323201-200739628244347="` echo /home/john.doe/.ansible/tmp/ansible-tmp-1756284934.1524658-2323201-200739628244347 `" ) && sleep 0'
Using module file /use/local/ansible-core/lib/python3.12/site-packages/ansible/modules/command.py
<localhost> PUT /home/john.doe/.ansible/tmp/ansible-local-2323086l5wencsk/tmpqeqqnev3 TO /home/john.doe/.ansible/tmp/ansible-tmp-1756284934.1524658-2323201-200739628244347/AnsiballZ_command.py
<localhost> EXEC /bin/sh -c 'chmod u+rwx /home/john.doe/.ansible/tmp/ansible-tmp-1756284934.1524658-2323201-200739628244347/ /home/john.doe/.ansible/tmp/ansible-tmp-1756284934.1524658-2323201-200739628244347/AnsiballZ_command.py && sleep 0'
<localhost> EXEC /bin/sh -c '/usr/bin/python /home/john.doe/.ansible/tmp/ansible-tmp-1756284934.1524658-2323201-200739628244347/AnsiballZ_command.py && sleep 0'
You can look at line 1 in each AnsiballZ_<module>.py file to see the full path to the Python interpreter being used.
- Since AnsiballZ_setup.py and AnsiballZ_file.py are on the target system, this means the Python CLI on the target server is being used to run the AnsiballZ files
- Since AnsiballZ_command.py are on localhost (the system Ansible is running on), this means the Python CLI on localhost (the system Ansible is running on) is being used to run the AnsiballZ file.
[jane.doe@server1 ~]$ head -1 /home/jane.doe/.ansible/tmp/ansible-tmp-1756288819.4388542-2481307-133499454356555/AnsiballZ_setup.py
#!/usr/bin/python
[jane.doe@server1 ~]$ head -1 /home/jane.doe/.ansible/tmp/ansible-tmp-1713408594.9606252-16563-253322922038816/AnsiballZ_file.py
#!/usr/bin/python
[john.doe@controller ~]$ head -1 /home/john.doe/.ansible/tmp/ansible-tmp-1756288823.0855155-2481766-234863562006181/AnsiballZ_command.py
#!/usr/bin/python3
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 (this article)
- Using the interpreter_python directive in ansible.cfg
Let's say the playbook includes ansible_python_interpreter: /usr/bin/python.
---
- hosts: all
vars:
ansible_python_interpreter: /usr/bin/python
tasks:
- name: create and then remove /tmp/foo.txt on target server
ansible.builtin.file:
path: /tmp/foo.txt
state: "{{ item }}"
with_items:
- touch
- absent
- name: run the ls command on localhost
ansible.builtin.shell: ls
delegate_to: localhost
...
This will cause the AnsiballZ files to have /usr/bin/python. Be aware that /usr/bin/python on the target server may be a different version of Python as /usr/bin/python on localhost (the system Ansible is running on).
[jane.doe@server1 ~]$ head -1 /home/jane.doe/.ansible/tmp/ansible-tmp-1756288819.4388542-2481307-133499454356555/AnsiballZ_setup.py
#!/usr/bin/python
[jane.doe@server1 ~]$ head -1 /home/jane.doe/.ansible/tmp/ansible-tmp-1713408594.9606252-16563-253322922038816/AnsiballZ_file.py
#!/usr/bin/python
[john.doe@controller ~]$ head -1 /home/john.doe/.ansible/tmp/ansible-tmp-1756288823.0855155-2481766-234863562006181/AnsiballZ_command.py
#!/usr/bin/python
Did you find this article helpful?
If so, consider buying me a coffee over at