A friend show me a problem when running Python script with ansible
command
module.
The command failed because the HOSTNAME
variable was missing. The ansible documentation warns about
this behaviour and provide this explanation: "The command(s) will not be processed through
the shell, so variables like $HOSTNAME and [...] will not work. Use the shell module if you need these features."
It's true, but there is a serious pitfall. The variable HOSTNAME is not really in the environment, you can't view
when running env
.
What's happened when running an ansible command
To troubleshoot, I'm using a simple Python script printenv.py
to print env:
#!/bin/python
import os
for k, v in os.environ.items():
print("{}={}".format(k, v))
print("{} variables found.".format(len(os.environ)))
I'm using a simple docker to simulate the remote host and demonstrate the problem.
FROM centos:7
RUN yum update -y
RUN yum -y install openssh-server openssh-clients sshpass
RUN mkdir /var/run/sshd
RUN echo 'root:r00t' | chpasswd
RUN sed -i 's/#PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd
RUN echo "export VISIBLE=now" >> /etc/profile
RUN /usr/bin/ssh-keygen -A
COPY printenv.py /root/
CMD "/usr/sbin/sshd" "-D"
Here is the output, and HOSTNAME is set:
docker exec -it d909a67ab429 python /root/printenv.py
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
TERM=xterm
HOSTNAME=d909a67ab429
HOME=/root
4 variables found.
When running ansible, even with the shell module you did not have access to HOSTNAME:
$ ansible -i docker.yml -m shell -a 'python /root/printenv.py' centos7
centos7 | SUCCESS | rc=0 >>
LANG=C
TERM=xterm-256color
SHELL=/bin/bash
LC_MESSAGES=C
MAIL=/var/mail/root
SHLVL=3
SSH_TTY=/dev/pts/1
PWD=/root
SSH_CLIENT=10.5.117.254 32896 22
LOGNAME=root
USER=root
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
LC_ALL=C
LS_COLORS=rs=0:di=38;5;27:ln=38;5;51:mh=44;38;5;15:pi=40;38;5;11:so=38;5;13:do=38;5;5:bd=48;5;232;38;5;11:cd=48;5;232;38;5;3:or=48;5;232;38;5;9:mi=05;48;5;232;38;5;15:su=48;5;196;38;5;15:sg=48;5;11;38;5;16:ca=48;5;196;38;5;226:tw=48;5;10;38;5;16:ow=48;5;10;38;5;21:st=48;5;21;38;5;15:ex=38;5;34:*.tar=38;5;9:*.tgz=38;5;9:*.arc=38;5;9:*.arj=38;5;9:*.taz=38;5;9:*.lha=38;5;9:*.lz4=38;5;9:*.lzh=38;5;9:*.lzma=38;5;9:*.tlz=38;5;9:*.txz=38;5;9:*.tzo=38;5;9:*.t7z=38;5;9:*.zip=38;5;9:*.z=38;5;9:*.Z=38;5;9:*.dz=38;5;9:*.gz=38;5;9:*.lrz=38;5;9:*.lz=38;5;9:*.lzo=38;5;9:*.xz=38;5;9:*.bz2=38;5;9:*.bz=38;5;9:*.tbz=38;5;9:*.tbz2=38;5;9:*.tz=38;5;9:*.deb=38;5;9:*.rpm=38;5;9:*.jar=38;5;9:*.war=38;5;9:*.ear=38;5;9:*.sar=38;5;9:*.rar=38;5;9:*.alz=38;5;9:*.ace=38;5;9:*.zoo=38;5;9:*.cpio=38;5;9:*.7z=38;5;9:*.rz=38;5;9:*.cab=38;5;9:*.jpg=38;5;13:*.jpeg=38;5;13:*.gif=38;5;13:*.bmp=38;5;13:*.pbm=38;5;13:*.pgm=38;5;13:*.ppm=38;5;13:*.tga=38;5;13:*.xbm=38;5;13:*.xpm=38;5;13:*.tif=38;5;13:*.tiff=38;5;13:*.png=38;5;13:*.svg=38;5;13:*.svgz=38;5;13:*.mng=38;5;13:*.pcx=38;5;13:*.mov=38;5;13:*.mpg=38;5;13:*.mpeg=38;5;13:*.m2v=38;5;13:*.mkv=38;5;13:*.webm=38;5;13:*.ogm=38;5;13:*.mp4=38;5;13:*.m4v=38;5;13:*.mp4v=38;5;13:*.vob=38;5;13:*.qt=38;5;13:*.nuv=38;5;13:*.wmv=38;5;13:*.asf=38;5;13:*.rm=38;5;13:*.rmvb=38;5;13:*.flc=38;5;13:*.avi=38;5;13:*.fli=38;5;13:*.flv=38;5;13:*.gl=38;5;13:*.dl=38;5;13:*.xcf=38;5;13:*.xwd=38;5;13:*.yuv=38;5;13:*.cgm=38;5;13:*.emf=38;5;13:*.axv=38;5;13:*.anx=38;5;13:*.ogv=38;5;13:*.ogx=38;5;13:*.aac=38;5;45:*.au=38;5;45:*.flac=38;5;45:*.mid=38;5;45:*.midi=38;5;45:*.mka=38;5;45:*.mp3=38;5;45:*.mpc=38;5;45:*.ogg=38;5;45:*.ra=38;5;45:*.wav=38;5;45:*.axa=38;5;45:*.oga=38;5;45:*.spx=38;5;45:*.xspf=38;5;45:
HOME=/root
_=/usr/bin/python
SSH_CONNECTION=10.5.117.254 32896 10.5.117.1 22
17 variables found.
Same thing happened when the env command:
ansible -i docker.yml -m shell -a 'env| grep HOSTNAME' centos7
centos7 | FAILED | rc=1 >>
non-zero return code
The hostname is the name of the container (2b97621d1f75) that docker have generated.
At this point we can see that there is a difference when running the script
printenv.py
through the docker or using in the ansible shell module.
The weirdest thing is that the variable HOSTNAME is defined!
ansible -i docker.yml -m shell -a 'echo $HOSTNAME/$USER' centos7
centos7 | SUCCESS | rc=0 >>
2b97621d1f75/root
It looks like that env command doesn't work as expected. UPDATE:
I received a message from Benoît explaining why there
is a difference between echo
, env
and the printenv.py
script. Simply the difference coming from that HOSTNAME
variable is set but is not exported. The export is done when reading when loading /etc/profile
.
[root@264421a1c781 /]# egrep -A 8 -Hnr 'HOSTNAME=' /etc/
/etc/profile:45:HOSTNAME=`/usr/bin/hostname 2>/dev/null`
/etc/profile-46-HISTSIZE=1000
[...]
/etc/profile-53-export PATH USER LOGNAME MAIL HOSTNAME HISTSIZE HISTCONTROL
But the /etc/profile
is not loaded when running ansible because ansible doesn't invocate
bash like an interactive shell neither like a login shell.
The cause of this behaviour
The problem is really complicated because there is a lot of layers involved when running ansible shell command.
- you are using a shell to run the ansible command. All the variables from the environment are not visible from the remote host
- ansible transform the command line ansible into a ssh command that could be see if you increase verbosity with
-vvv
. It looks like<10.5.117.1> SSH: EXEC sshpass -d12 ssh -C -o ControlMaster=auto -o ControlPersist=60s -o User=root -o ConnectTimeout=10 -o ControlPath=/home/rjacquet/.ansible/cp/634b52de7c -tt 10.5.117.1
- the ssh is using a bash command to run the ansible module. Note that the bash command is not a shell login, this is why accessing to HOSTNAME variable is not possible. Stuffs like are also visible with more verbosity
/bin/sh -c '"'"'rm -f -r /root/.ansible/tmp/ansible-tmp-1603741018.49-107090793072670/ > /dev/null 2>&1 && sleep 0
- the bash (non-login) shell is running a python command with the syscall fork
- finally the Python command use the subprocess module to run
printenv.py
! Furthermore the subprocess module doesn't inherit the environment from its parent (the sh scripts). The environment is created is the ansible classAnsibleModule
and therun_command
method after the comment# Manipulate the environ we'll send to the new process
(see here). Probably you have never heard about this module except you have already written your own ansible module. This a class that provide common things to all modules.
Simple workaround
The only workaround I found is to change the python or shell script to run the hostname
command
instead of using the environment. This is exactly how the shell initialize the variable.
If you have a solution without changing the script, feel free to share me the solution :-).