I want to write a playbook which check if a file exist for all users including root and if this file exists make some changes to this file. I have a script getUsers.fact which will get all users with uid 1000 and above.
I came out with the playbook below but of course it won’t work because of the first task which check if the file exist for all users before going to the second task which will do something with the file if it exists.
How can I loop all the users and check if the file exists, modify the file and go to the next user?
---
- name: make changes to file for all users
hosts: all
tasks:
- name: Check state of file .bashrc
ansible.builtin.stat:
path: /home/{{ item }}/.bashrc
register: fileStat
with_items:
- "{{ ansible_facts.ansible_local.getUsers.user }}"
- root
- name: List file
ansible.builtin.command:
cmd: excute.changes.on.files
with_items:
- "{{ ansible_facts.ansible_local.getUsers.user }}"
- root
when: fileStat.stat.exists
...
By the way, I am new to Ansible and presently reading the doc but need to fix things now.
---
- name: make changes to file for all users
hosts: all
tasks:
- name: Check state of file .bashrc for each user and modify if exists
block:
- name: Check state of file .bashrc for {{ item }}
ansible.builtin.stat:
path: "{{ '/root' if item == 'root' else '/home/' + item }}/.bashrc"
register: fileStat
- name: Modify file for {{ item }} if it exists
ansible.builtin.command:
cmd: excute.changes.on.files
when: fileStat.stat.exists
loop:
- "{{ ansible_facts.ansible_local.getUsers.user }}"
- root
I’m trying to reproduce your issue, and I’m stuck coming up with a /etc/ansible/facts.d/getUsers.fact that works with your expression {{ ansible_facts.ansible_local.getUsers.user }}. I’m curious what your getUsers.fact looks like. Here’s mine:
#!/bin/bash
# /etc/ansible/facts.d/getUsers.fact
# Returns JSON of local users with id>999 and without a 'nologin' shell.
pending=''
echo -n '['
getent passwd | while IFS=: read -r user password uid gid gecos home shell; do
if [ "$uid" -gt 999 ] && [[ ! "$shell" =~ nologin ]] ; then
printf "%s{" "$pending"
printf '"user": "%s", ' "$user"
printf '"uid": "%s", ' "$uid"
printf '"home": "%s", ' "$home"
printf '"shell": "%s"}' "$shell"
pending=$',\n'
fi
done
echo ']'
Having typed all that up and reading over it, it dawned on me that maybe your ansible_facts.ansible_local.getUsers.user expression isn’t working for you either.
Grouping tasks with blocks
All tasks in a block inherit directives applied at the block level. Most of what you can apply to a single task (with the exception of loops) can be applied at the block level, so blocks make it much easier to set data or directives common to the tasks.
Here the output If I try to run it:
$ ansible-playbook playbooks.d/playbook_bashrc.yaml
ERROR! 'loop' is not a valid attribute for a Block
In fact, your first method finds my wife’s id (tplewis), but misses mine (utoddl) because my uid doesn’t start with (or contain) 10. Behold:
[utoddl@tango ansible]$ for u in `grep 10[0-9][0-9] /etc/passwd | awk -F: '{print $1}'` ; do echo " \"user\": \"${u}\""; done;
"user": "tplewis"
[utoddl@tango ansible]$ getent passwd tplewis utoddl
tplewis:x:1001:1001::/home/tplewis:/bin/bash
utoddl:x:12428:12428:Todd Lewis:/home/utoddl:/bin/bash
But besides that, the expression ansible_facts.ansible_local.getUsers.user is only going to work if there’s a single user. If that’s a list of users, it’s going to require more jinja.
- name: Show getUsers
ansible.builtin.debug:
msg: "{{ ansible_facts.ansible_local.getUsers }}"
- name: Show each user returned by getUsers
ansible.builtin.debug:
msg: "{{ item }}"
loop:
- "{{ ansible_facts.ansible_local.getUsers | map(attribute='user') }}"
- name: Show each user returned by getUsers plus 'root'
ansible.builtin.debug:
msg: "{{ item }}"
loop:
- "{{ ansible_facts.ansible_local.getUsers | map(attribute='user') + ['root'] }}"
So to loop over “those users” plus “root”, you could use that last expression:
Otherwise, you get one item which itself is a list. This way, you get one user per item.
There’s an aesthetic balance, a trade-off to be made, between complexity (i.e. number of pieces) and complications (i.e. how intricate individual pieces are). Adding another file to have something to loop over increases complexity with no particular gain — except that it’s the only way to get Ansible to do the thing, which is frankly a design flaw in Ansible so I’m glossing over that point for this argument.
“Don’t do in shell scripts what you can do in tasks” is a guideline, not a rule. Choosing when to follow it depends on lots of factors, including who’s going to run and maintain the playbook, overall comfort with and understanding of various failure modes, comfort level with shell scripting generally, etc.
For me, looking at your original ask, I’d be happier with one Ansible task that uses a shell script, something like this:
- name: Do a thing at/with/to these users' .bashrc files
ansible.builtin.shell: |
IFS=: read -r user password uid gid gecos home shell < <(getent passwd '{{ item }}')
if [ -f "$home/.bashrc" ] ; then
echo "do a thing with '$home/.bashrc'"
# This is where your 'excute.changes.on.files' command goes
exit $?
else
echo "no '$home/.bashrc' or can't access it." >&2
exit 0
fi
args:
executable: /bin/bash
loop: "{{ ansible_facts.ansible_local.getUsers | map(attribute='user') + ['root'] }}"
- name: make changes to file for all users
hosts: all
tasks:
- name: Block of tasks
ansible.builtin.include_tasks: looping_ansible_facts_tasks.yaml
loop:
This has been very enlightening for me. This make me get some new concepts about Ansible.
Hello Todd,
I found a little problem with script getUsers. Here is the corrected version:
declare -x -a UARR
# Build array of users
while IFS=: read -r user password uid gid gecos home shell ; do
if (( ${uid} > 999 )) && [[ ! ${shell} =~ nologin ]] ; then
UARR=( ${UARR[*]} ${user} )
fi
done < <( getent passwd )
# Build json output
printf "{ \"users\": ["
x=0
while (( x < ${#UARR[*]} )); do
(( ${x} == ${#UARR[*]}-1 )) && printf "\"${UARR[${x}]}\"" || printf "\"${UARR[${x}]}\", "
(( x++ ))
done
printf " ] }\n"
I found it is a good idea to check the output of the script above with jq to validate if it is a proper json format. Also, we can see it is properly working with the module setup and listing the facts about a host.
Now I have to found a way to iterate a json array. My script create a json array of users.
I like this better. It’s shorter, doesn’t double your last entry, nor assume there is one.
printf '{ "users": ['
sep=''
while IFS=: read -r user password uid gid gecos home shell ; do
if (( "${uid}" > 999 )) && [[ ! "${shell}" =~ nologin ]] ; then
printf '%s"%s"' "${sep}" "${user//\"/\\\"}"
sep=', '
fi
done < <( getent passwd )
printf "] }\n"
It also escapes double-quotes in the $user which is required in the generated json. Probably not going to be an issue here, but the technique is instructive. I learned something anyway, so thanks for that.
Finally, get in the habit of linting any shell scripts you write with the most excellent shellcheck program. It can elevate a pretty good shell scripter to the next level. Likewise for ansible-lint and Ansible. Nobody has enough time to skip linting.
That’s the beauty of “facts”. Your playbook doesn’t need to care what form your data arrived in when it was ingested. It’s just a list, like any other. So now you can put your tasks in a separate file — let’s call it bash_rc_wrangler.yml — and you can iterate over it like this:
- name: Run the bash_rc_wrangler tasks on each local user and "root"
ansible.builtin.include_tasks:
file: bash_rc_wrangler.yml
loop: "{{ ansible_facts.ansible_local.getUsers + ['root'] }}"
loop_control:
loop_var: wrangle_user
Those last two lines create the variable wrangle_user and sets it to each of your users’ userids. The tasks inside bash_rc_wrangler.yml will use that variable to know what user they are working on. The other thing this buys you is, within that task file, if you loop over some other list, you can let it set the default loop_var item without wiping out your wrangle_user variable. Without those lines, your users’ userids would be in item, which is overwritten by the next loop that comes along.