ansible.builtin.file is using user from wrong host - ansible

I have this playbook below to set user/group on the user's home directory.
jimbo and bobo here have different UID and GIDs on the different boxes.
Running this script will set the UID/GID ownership of the directories incorrectly.
For example, it will set /home/jimbo on operatorbox1 (1) to be owned by the UID of jimbo from operatorbox2 (2) - which is of course not the correct UID on operatorbox1 (1).
It does this seemingly randomly. If I run this playbook multiple times the ownership of the directories will flip back and forth.
Guessing I have something fundamental missing here. Why is this happening? Thanks!
ansible-playbook v2.9.23
./vars/operators.yml
---
operators:
jimbo: sshekeywhatever
bobo: sshkeywhatever
playbook.yml
---
- name: Setup operators
hosts:
- bastionbox
- operatorbox
become: true
vars_files:
- "./vars/operators.yml"
tasks:
- name: Set home directory permissions
file:
path: "/home/{{ item.key }}"
state: directory
owner: "{{ item.key }}"
group: "{{ item.key }}"
recurse: true
with_dict:
- "{{ operators }}"

I can't reproduce the problem. Below is a playbook for testing
- hosts: bastionbox,operatorbox
gather_facts: false
become: true
vars:
operators: [jimbo, bobo]
tasks:
- name: Create users
user:
name: "{{ item }}"
shell: /usr/sbin/nologin
uid: "{{ range(2500, 2600)|random }}"
loop: "{{ operators }}"
when: create_users|d(false)|bool
- name: List users uid
block:
- getent:
database: passwd
- debug:
msg: "{{ inventory_hostname }} {{ item }} uid: {{ getent_passwd[item].1 }}"
loop: "{{ operators }}"
when: list_users|d(false)|bool
- name: Set home directory owner and group
file:
state: directory
path: "/home/{{ item }}"
owner: "{{ item }}"
group: "{{ item }}"
recurse: true
loop: "{{ operators }}"
when: set_homes|d(false)|bool
- name: List homes
block:
- find:
paths: /home
file_type: directory
patterns: "{{ operators }}"
register: out
- debug:
msg: "{{ inventory_hostname }} {{ item.path }} uid: {{ item.uid }}"
loop: "{{ out.files }}"
loop_control:
label: "{{ inventory_hostname }}"
when: list_homes|d(false)|bool
- name: Delete users
user:
name: "{{ item }}"
state: absent
remove: true
loop: "{{ operators }}"
when: delete_users|d(false)|bool
Create users
shell> ansible-playbook -e create_users=true pb.yml
List users
shell> ansible-playbook -e list_users=true pb.yml
msg: 'bastionbox jimbo uid: 2572'
msg: 'operatorbox jimbo uid: 2537'
msg: 'bastionbox bobo uid: 2505'
msg: 'operatorbox bobo uid: 2557'
List homes
shell> ansible-playbook -e list_homes=true pb.yml
msg: 'bastionbox /home/bobo uid: 2505'
msg: 'operatorbox /home/jimbo uid: 2537'
msg: 'bastionbox /home/jimbo uid: 2572'
msg: 'operatorbox /home/bobo uid: 2557'
Set homes (task is idempotent)
shell> ansible-playbook -e set_homes=true pb.yml
TASK [Set home directory owner and group] *************************
ok: [operatorbox] => (item=jimbo)
ok: [bastionbox] => (item=jimbo)
ok: [operatorbox] => (item=bobo)
ok: [bastionbox] => (item=bobo)

Related

Nested loop with user and folder in Ansible

I have the following task:
- name: Create required folders.
become: true
ansible.builtin.file:
owner: "{{ item.key }}"
group: ftp
mode: '0755'
path: '/data/{{ item.key }}/in'
state: directory
loop: "{{ query('dict', ftp) | list }}"
when: "'state' not in item.value or item.value.state == 'present'"
And the following host variables with different users:
ftp:
test:
ssh_public_key: "XXXX"
password: "XXX"
home: /data/test
test2:
ssh_public_key: "XXXX"
password: "XXX"
home: /data/test2
What I want is to create two directories for every user :
path: '/data/{{ user }}/in' # item.key, in the code above
path: '/data/{{ user }}/out' # item.key, in the code above
But I already need the loop for iterating over the users itself:
loop: "{{ query('dict', ftp) | list }}"
How can I handle this, for example, with nested loop?
Use a product filter to generate every possible combination of user/folder.
loop: "{{ ftp.keys() | product(['in', 'out']) }}"
Then, respectively,
item.0 contains the users dictionary keys
item.1 contains the folders
It is not fully clear what your condition when does actually, in order to adapt it too, but I guess that you do have an absent or present state in those use dictionaries.
So, the resulting task should be something along the lines of
- name: Create required folders
ansible.builtin.file:
owner: "{{ item.0 }}"
group: ftp
mode: '0755'
path: "/data/{{ item.0 }}/{{ item.1 }}"
state: directory
loop: "{{ ftp.keys() | product(['in', 'out']) }}"
loop_control:
label: "/data/{{ item.0 }}/{{ item.1 }}"
when: "ftp[item.0].state | default('absent') == 'present'"
become: true
Given the task above, when run on those data:
ftp:
test:
state: present
test1:
test2:
state: present
It will yield:
TASK [Create required folders] ***************************************
ok: [localhost] => (item=/data/test/in)
ok: [localhost] => (item=/data/test/out)
skipping: [localhost] => (item=/data/test1/in)
skipping: [localhost] => (item=/data/test1/out)
ok: [localhost] => (item=/data/test2/in)
ok: [localhost] => (item=/data/test2/out)
Test it first, for example
- debug:
msg: "Create /data/{{ item.0.key }}/{{ item.1 }}"
with_nested:
- "{{ ftp|dict2items }}"
- [in, out]
when: item.0.value.state|d('present') == 'present'
gives (abridged)
msg: Create /data/test/in
msg: Create /data/test/out
msg: Create /data/test2/in
msg: Create /data/test2/out
Then try to create the dictionaries
- file:
owner: "{{ item.0.key }}"
group: ftp
mode: '0755'
path: "/data/{{ item.0.key }}/{{ item.1 }}"
state: directory
with_nested:
- "{{ ftp|dict2items }}"
- [in, out]
when: item.0.value.state|d('present') == 'present'
(not tested)

ansible variable basing on hostname's suffix

I need to create a user on a system depending on it's function (test or production)
Test systems are named with suffix -tXX while prod ones are with -pXX (where XX is two digits numbering).
in variable file I have set:
auser: "{{ 'usertest' if {{ ansible_hostname|lower }} | regex_search('t[0-9]{2}$') else 'userprod' }}"
the error I get is during playbook run is:
fatal: [192.168.1.10]: FAILED! => {"msg": "An unhandled exception occurred while templating '{{ 'usertest' if {{ ansible_hostname|lower }} | regex_search('t[0-9]{2}$') else 'userprod' }}'. Error was a <class 'ansible.errors.AnsibleError'>, original message: template error while templating string: expected token ':', got '}'. String: {{ 'usertest' if {{ ansible_hostname|lower }} | regex_search('t[0-9]{2}$') else 'userprod' }}"}
Complete playbook is:
---
- hosts: testgrp
become: yes
vars:
- auser: "{{ 'usertest' if {{ ansible_hostname|lower }} | regex_search('t[0-9]{2}$') else 'userprod' }}"
tasks:
- name: Add groups
group:
name: "{{ item.name }}"
gid: "{{ item.gid }}"
state: present
loop:
- { name: 'group1', gid: '1101' }
- { name: 'group2', gid: '1102' }
- { name: 'group3', gid: '1103' }
- name: Add users
user:
name: "{{ item.name }}"
group: "{{ item.group }}"
groups: "{{ item.groups }}"
state: present
loop:
- { name: "{{ auser }}", group: 'group1', groups: 'group2,group3', uid: '1101' }
For example
- hosts: srv-t01,srv-p01
gather_facts: false
vars:
auser: "{{ inventory_hostname | lower is match('^.*-t[0-9]{2}$') |
ternary('usertest', 'userprod') }}"
tasks:
- debug:
var: auser
gives
ok: [srv-t01] =>
auser: usertest
ok: [srv-p01] =>
auser: userprod
A more robust solution is to select an index, e.g. the play below gives the same result
- hosts: srv-t01,srv-p01
gather_facts: false
vars:
auser_dict:
t: usertest
p: userprod
aindex: "{{ inventory_hostname | lower |
regex_replace('^.*-(.+)[0-9]{2}$', '\\1') }}"
auser: "{{ auser_dict[aindex] }}"
tasks:
- debug:
var: auser

In Ansible loop, test existence of files from registered results

I have several files that I need to backup in different directories. I have tried the code below and not working for me.
vars:
file_vars:
- {name: /file1}
- {name: /etc/file2}
- {name: /etc/file/file3}
tasks:
- name: "Checking if config files exists"
stat:
path: "{{ item.name }}"
with_items: "{{ file_vars }}"
register: stat_result
- name: Backup Files
copy: src={{ item.name }} dest={{ item.name }}{{ ansible_date_time.date }}.bak
with_items: "{{ file_vars }}"
remote_src: yes
when: stat_result.stat.exists == True
The problem is the condition
when: stat_result.stat.exists == True
There is no attribute stat_result.stat. Instead, the attribute stat_result.results is a list of the results from the loop. It's possible to create a dictionary of files and their statuses. For example
- set_fact:
files_stats: "{{ dict(my_files|zip(my_stats)) }}"
vars:
my_files: "{{ stat_result.results|json_query('[].item.name') }}"
my_stats: "{{ stat_result.results|json_query('[].stat.exists') }}"
Then simply use this dictionary in the condition
when: files_stats[item.name]
Below is a shorter version which creates the dictionary more efficiently
- set_fact:
files_stats: "{{ dict(stat_result.results|
json_query('[].[item.name, stat.exists]')) }}"
Please try using below worked for me:
---
- name: Copy files
hosts: localhost
become: yes
become_user: root
vars_files:
- files.yml
tasks:
- name: "Checking if config files exists"
stat:
path: "{{ item }}"
with_items: "{{ files }}"
register: stat_result
- name: Ansible
debug:
msg: "{{ stat_result }}"
- name: Backup Files
copy:
src: "{{ item }}"
dest: "{{ item.bak }}"
with_items: "{{ files }}"
when: stat_result == "True"
and files.yml will look like:
---
files:
- /tmp/file1
- /tmp/file2
you can check you playbook syntax using below command:
ansible-playbook copy.yml --syntax-check
Also you do dry run your playbook before actual execution.
ansible-playbook -i localhost copy.yml --check

How to create a 'null' default in Ansible

I want 'lucy' to follow the user module creators' default behaviour which is to create and use a group matching the user name 'lucy'. However for 'frank' I want the primary group to be an existing one; gid 1003. So my hash looks like this:
lucy:
comment: dog
frank:
comment: cat
group: 1003
And my task looks like this:
- name: Set up local unix user accounts
user:
name: "{{ item.key }}"
comment: "{{ item.value.comment }}"
group: "{{ item.value.group | default(undef) }}"
loop: "{{ users|dict2items }}"
This doesn't work, as undef is not recognised. Nor is anything else I can think of. 'null', 'None' etc. all fail. '' creates an empty string which is not right either. I can't find out how to do it.
Any ideas?
default(omit) is what you are looking for. For example,
- name: Set up local Unix user accounts
user:
name: "{{ item.key }}"
comment: "{{ item.value.comment }}"
group: "{{ item.value.group | default(omit) }}"
loop: "{{ users|dict2items }}"
Comments
Comment by Lucas Basquerotto: "... omit only works correctly when used directly in a module, it won't work in a set_fact ..."
A: You're wrong. For example, default(omit) works both in set_fact and in the module. The first item in the list defaults to false with the result "VARIABLE IS NOT DEFINED!". The second item defaults to omit. Omitted parameter get_checksum defaults to true with the checksum in the results
shell> cat pb.yml
- hosts: localhost
tasks:
- set_fact:
test:
- "{{ gchk|default(false) }}"
- "{{ gchk|default(omit) }}"
- stat:
path: /etc/passwd
get_checksum: "{{ item }}"
loop: "{{ test }}"
register: result
- debug:
var: item.stat.checksum
loop: "{{ result.results }}"
gives
shell> ansible-playbook pb.yml | grep item.stat.checksum
item.stat.checksum: VARIABLE IS NOT DEFINED!
item.stat.checksum: 7c73e9f589ca1f0a1372aa4cd6944feec459c4a8
In addition to this, default(omit) works as expected also in some expressions. For example
- debug:
msg: "{{ {'a': item}|combine({'b': true}) }}"
loop: "{{ test }}"
gives
msg:
a: false
b: true
msg:
b: true
See the results without default values
shell> ansible-playbook pb.yml -e "gchk={{ true|bool }}"

Looping in hostvars

I'm wondering if it is possible to perform a loop in the hostvars folder when using Ansible?
Here is what I've tried but haven't had success in making it work - or is it just not possible to do?
---
list_pool: 'list ltm pool {{ items }}'
with_items:
- 'abc123'
- 'def456'
I would use the "list_pool" variable in a playbook afterward:
- name: List pool
bigip_command:
server: "{{ some_server }}"
user: "{{ some_user }}"
password: "{{ some_password }}"
commands:
- "{{ list_pool }}"
validate_certs: no
delegate_to: localhost
Not sure what you mean when you say you want to loop over hostvars folder.
From what I can interpret from your tasks is: "You need to execute big-ip command list ltm <pool-name> for multiple pools in the list list_pool"
If that's what you're after, this should work:
- name: Set list_pool fact
set_fact:
list_pool: "{{ list_pool | default([]) + [item] }}"
with_items:
- 'abc123'
- 'def456'
- name: List pool
bigip_command:
server: "{{ some_server }}"
user: "{{ some_user }}"
password: "{{ some_password }}"
commands:
- "list ltm {{ item }}"
validate_certs: no
delegate_to: localhost
with_items: "{{ list_pool }}"
I got this working with the following solution:
hostvars file would look like this:
---
pre_checks:
checks:
pool:
- name: "pool_123"
- name: "pool_456"
...
And the play would look like this:
--output truncated---
- name: Fetch device host_vars
set_fact:
device_config: "{{ ((lookup('file','{{playbook_dir}}/host_vars/{{inventory_hostname}}.yml')) | from_yaml) }}"
- name: Check pool
bigip_command:
server: "{{ inventory_hostname }}"
user: "{{ remote_username }}"
password: "{{ remote_passwd }}"
commands:
- "list ltm pool {{ item }}"
validate_certs: no
with_items:
- "{{ device_config.pre_checks | json_query('checks.pool[*].name') }}"
delegate_to: localhost
when: "'active' in Active_LTM['stdout'][0]"
register: Pre_Checks
- name: Verify pool
debug: var=item
with_items: "{{ Pre_Checks.results | map(attribute='stdout_lines') | list }}"

Resources