Ansible line-in-file re-adding entries - ansible

What I'm trying to solve: I have several servers (in this example 3, 1 nfs server, 2 clients - they all resolve back to localhost for this minimal example), and the clients need to access shares on the server, which are created using the playbook.
The IP addresses of the clients need to be added to the respective entries in /etc/exports as they go - the list is not predefined at any given time. (In my actual playbook I use ansible facts, for this example I've added them as a variable)
In Ansible lineinfile regexp to manage /etc/exports Vladimir was so kind as to help with an initial thing, which works, but doesn't seem to be idempotent. The entries / ip addresses are added correctly the first time round, but the second run the IP addresses get re-added to the entries in /etc/exports, causing nfs to bomb out at that time (double entries)
Correct /etc/exports:
bar 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check)
foo 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check)
What I'm getting:
bar 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check) 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check)
foo 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check) 192.168.34.47(rw,sync,no_root_squash,no_subtree_check) 192.168.34.46(rw,sync,no_root_squash,no_subtree_check)
I've been butting my head around it but I can't come up with anything that works. I've distilled it down to the following playbook:
$ ansible-playbook main.yml
Content of the main.yml:
- hosts: localhost
become: yes
vars:
client_ip: 192.168.34.46
nfs_server: localhost
shares:
- bar
- foo
tasks:
- name: Mountpoint management
ansible.builtin.include_tasks: task.yml
loop: "{{ shares | default([]) }}"
loop_control:
loop_var: volume
args:
apply:
delegate_to: "{{ nfs_server }}"
- hosts: localhost
become: yes
vars:
client_ip: 192.168.34.47
nfs_server: localhost
shares:
- bar
- foo
tasks:
- name: Mountpoint management
ansible.builtin.include_tasks: task.yml
loop: "{{ shares | default([]) }}"
loop_control:
loop_var: volume
args:
apply:
delegate_to: "{{ nfs_server }}"
Second file task.yml which is needed to do a loop:
---
---
- name: Ensure /etc/exports exists
ansible.builtin.file:
path: /etc/exports
owner: root
group: root
mode: '0644'
state: touch
changed_when: False
- name: Add host {{ client_ip }} to {{ volume }}
ansible.builtin.lineinfile:
path: "/etc/exports"
regex: '^{{ volume }}(\s+)({{ ip_regex }})*({{ mount_opts_regex }})*(\s*)(.*)$'
line: '{{ volume }}\g<1>{{ ip }}{{ mount_opts }} \g<5>'
backrefs: true
vars:
ip: "{{ client_ip }}"
ip_regex: '{{ client_ip | regex_escape() }}'
mount_opts: '(rw,sync,no_root_squash,no_subtree_check)'
mount_opts_regex: '\(.*?\)'
- name: Read /etc/exports
command: "cat {{ item }}"
register: result
check_mode: no
loop:
- /etc/exports
changed_when: False
- ansible.builtin.set_fact:
content: "{{ dict(_files|zip(_lines)) }}"
vars:
_lines: "{{ result.results|map(attribute='stdout_lines')|list }}"
_files: "{{ result.results|map(attribute='item')|list }}"
- name: Add new line to /etc/exports
ansible.builtin.lineinfile:
path: "/etc/exports"
line: '{{ volume }} {{ client_ip }}{{ mount_opts }}'
vars:
mount_opts: '(rw,sync,no_root_squash,no_subtree_check)'
loop: "{{ content }}"
when: content[item] | select('search', volume)|length == 0

Well, thinking it through I finally went with constructing two lists, mapping those into a dict and checking if the values are already present. If they are, don't do anything, if not, add it.
main.yaml:
- hosts: localhost
become: yes
vars:
client_ip: 192.168.34.46
nfs_server: localhost
shares:
- bar
- foo
tasks:
- name: Mountpoint management
ansible.builtin.include_tasks: task.yml
loop: "{{ shares | default([]) }}"
loop_control:
loop_var: volume
args:
apply:
delegate_to: "{{ nfs_server }}"
- hosts: localhost
become: yes
vars:
client_ip: 192.168.34.47
nfs_server: localhost
shares:
- bar
- foo
tasks:
- name: Mountpoint management
ansible.builtin.include_tasks: task.yml
loop: "{{ shares | default([]) }}"
loop_control:
loop_var: volume
args:
apply:
delegate_to: "{{ nfs_server }}"
task.yaml:
---
- name: Ensure /etc/exports exists
ansible.builtin.file:
path: /etc/exports
owner: root
group: root
mode: '0644'
state: touch
changed_when: false
- name: Read /etc/exports
command: "cat /etc/exports"
register: result
check_mode: no
changed_when: false
- ansible.builtin.set_fact:
share_names: "{{ share_names | default([]) + [item.split(' ')[0]] }}"
share_values: "{{ share_values | default([]) + [item.split(' ')[1:]] }}"
loop:
"{{ result.stdout_lines }}"
- ansible.builtin.set_fact:
share_content: "{{ dict(share_names | zip(share_values)) }}"
- name: Add host {{ client_ip }} to {{ volume }}
ansible.builtin.lineinfile:
path: "/etc/exports"
regex: '^{{ volume }}(\s+)({{ ip_regex }})*({{ mount_opts_regex }})*(\s*)(.*)$'
line: '{{ volume }}\g<1>{{ client_ip }}{{ mount_opts }} \g<5>'
backrefs: true
vars:
ip_regex: '{{ client_ip | regex_escape() }}'
mount_opts: '(rw,sync,no_root_squash,no_subtree_check)'
mount_opts_regex: '\(.*?\)'
str: '{{ client_ip }}{{ mount_opts }}'
when: volume in share_content and str not in share_content[volume]
- name: Add new line to /etc/exports
ansible.builtin.lineinfile:
path: "/etc/exports"
line: '{{ volume }} {{ client_ip }}{{ mount_opts }}'
vars:
mount_opts: '(rw,sync,no_root_squash,no_subtree_check)'
loop: "{{ share_content }}"
when: volume not in share_content

Related

Ansible - loop over multiple items in stdout_lines

I am performing a grep with multiple items.
---
- hosts: my_host
gather_facts: false
vars:
my_list:
- whatever
- something
tasks:
- name: grep for item in search path
shell: "grep -rIL {{ item }} /tmp"
register: the_grep
loop: "{{ my_list }}"
- debug:
msg: "{{ item.stdout_lines }}"
loop: "{{ the_grep.results }}"
Depending on the result, multiple files could match.
msg:
- /tmp/something.conf
- /tmp/folder/file.txt
Q: How would I configure Ansible to loop over the items in stdout_lines?
The use case I'm solving is to delete .ini sections based on the item, but in this case, Ansible doesn't loop over the stdout_lines.
- name: remove stanza from ini file
ini_file:
path: "{{ item.stdout_lines }}"
section: "{{ item.item }}"
mode: '0600'
state: absent
loop: "{{ the_grep.results }}"
when: item.stdout_lines | length > 0
It seems that this doesn't work, but configuring item.stdout_lines[0] gives the partially expected result, since Ansible will use only the first item in that list. But ofc, not the 2nd and so on.
Perhaps there's a prettier answer, but solved it by using with_nested and creating a json_query:
- name: remove stanza from ini file
ini_file:
path: "{{ item.0 }}"
section: "{{ item.1.item }}"
mode: '0600'
state: absent
with_nested:
- "{{ the_grep | json_query('results[].stdout_lines[]') }}"
- "{{ the_grep.results }}"

ansible.builtin.file is using user from wrong host

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)

Ansible variables question from output and filtered with json_query

I am trying to get the pertinent fields from a vmware module output, I am filtering with json, but the says my variable target_vm_name is undefined. how do I get a variable with only the matching criteria which is filtered by the ip address of the list of hosts I want to iterate through?
code is below:
- name: Loop thru hosts and get IPs
hosts: rpa_test
vars:
ip_addr:
"{{ lookup('dig', ansible_host) }}"
tasks:
- debug:
msg: "System {{ inventory_hostname }} has ip address of {{ ip_addr }}"
- name: get vm info based on ip
community.vmware.vmware_vm_info:
hostname: "{{ vcenter_hostname }}"
username: "{{ vault_vcenter_admin_name }}"
password: "{{ vault_vcenter_admin_password }}"
validate_certs: False
delegate_to: localhost
register: vm_info
vars:
target_vm_name: "{{ vm_info.virtual_machines | json_query(query) }}"
query: "[?ip_address=={{ ip_addr }}]"
- debug:
msg: "{{ target_vm_name.virtual_machines.guest_name }}"
vars is not at the right place inside the community.vmware.vmware_vm_info task ,
you should put it in the debug task:
tasks:
- debug:
msg: "System {{ inventory_hostname }} has ip address of {{ ip_addr }}"
- name: get vm info based on ip
community.vmware.vmware_vm_info:
hostname: "{{ vcenter_hostname }}"
username: "{{ vault_vcenter_admin_name }}"
password: "{{ vault_vcenter_admin_password }}"
validate_certs: False
delegate_to: localhost
register: vm_info
- debug:
msg: "{{ target_vm_name.virtual_machines.guest_name }}"
vars:
target_vm_name: "{{ vm_info.virtual_machines | json_query(query) }}"
query: "[?ip_address=={{ ip_addr }}]"

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 }}"

Ansible, iterate over multiple registers and make extended use of build in methods

To start off I have all my variables defined in YAML
app_dir: "/mnt/{{ item.name }}"
app_dir_ext: "/mnt/{{ item.0.name }}"
workstreams:
- name: tigers
service_workstream: tigers-svc
app_sub_dir:
- inbound
- inbound/incoming
- inbound/error
- inbound/error/processed
- name: lions
service_workstream: lions-svc
app_sub_dir:
- inbound
- inbound/incoming
- inbound/error
- inbound/error/processed
You may note app_dir: "/mnt/{{ item.name }}" and app_dir_ext: "/mnt/{{ item.0.name }}" looking odd, so I originally had my variables set as below in YAML but decided to use the above mainly due to less lines in YAML when I have a large amount of workstreams.
workstreams:
- name: tigers
service_workstream: tigers-svc
app_dir: /mnt/tigers
...
I then have Ansible code to check if the directories exists, if not create them and apply permissions (!note, have taken this approach due to a ssh timeout on operation when using the file: module on a number of very big NFS mounted shares).
- name: Check workstreams app_dir
stat:
path: "{{ app_dir }}"
register: app_dir_status
with_items:
- "{{ workstreams }}"
- name: Check workstreams app_sub_dir
stat:
path: "{{ app_dir_ext }}/{{ item.1 }}/"
register: app_sub_dir_status
with_subelements:
- "{{ workstreams }}"
- app_sub_dir
- name: create workstreams app_dir
file:
path: "/mnt/{{ item.0.name }}"
state: directory
owner: "ftp"
group: "ftp"
mode: '0770'
recurse: yes
with_nested:
- '{{ workstreams }}'
- app_dir_status.results
when:
- '{{ item.1.stat.exists }} == false'
This is a little hacky but works, however I have a 2nd, 3rd, 4th path to check..etc
My question here is how to I update/refactor the above code to use <register_name>.stat.exists == false from both app_dir_status and app_sub_dir_status to control my task ?
You don't need to make nested loop! There's all required data inside app_sub_dir_status – just strip unnecessary items.
Here's simplified example:
---
- hosts: localhost
gather_facts: no
vars:
my_list:
- name: zzz1
sub:
- aaa
- ccc
- name: zzz2
sub:
- aaa
- bbb
tasks:
- stat:
path: /tmp/{{ item.0.name }}/{{ item.1 }}
register: stat_res
with_subelements:
- "{{ my_list }}"
- sub
- debug:
msg: "path to create '{{ item }}'"
with_items: "{{ stat_res.results | rejectattr('stat.exists') | map(attribute='invocation.module_args.path') | list }}"
You can iterate over stat_res.results | rejectattr('stat.exists') | list as well, but will have to construct path again as /tmp/{{ item.item.0.name }}/{{ item.item.1 }} – note double item, because first item is an element of stat_res.results which contain another item as element of your original loop for stat task.
P.S. Also I see no reason for your first task, as subdir task can detect all missing directories.

Resources