Can items in Ansible loop or with_items be processed concurrently? - loops

- block:
- name: ensure backup directory exists
win_file:
path: "{{ BACKUP }}"
state: directory
recurse: yes
- include_tasks: increment_backup.yml
with_items: "{{ filelist.stdout_lines }}"
when: work_status.stat.exists
I just want these items be dealt with concurrently.
For example, there are 10 items within filelist.stdout_lines. What I wanna do is these 10 items can be processed parallel.
It's not task level. It's within one single task.
But I didn't find any clues from Ansible documents.
Does anybody know about this?

Related

Ansible: loop trough list of files of different users, sort by date, delete oldest x ones of each user

I am struggling with this issue since several days and would appreciate your help. I would like to check if the crontab file for several users exist on a host. If so, a backup shall be done and the oldest x backups shall be removed.
To achieve this I first check if the crontab exists:
- name: check if crontab exists
ansible.builtin.stat:
path: "/var/spool/cron/tabs/{{ item }}"
loop: "{{ users }}"
register: crontabFile
Then I perform a backup of the file:
- name: backup old crontab
ansible.builtin.copy:
src: "{{ item.stat.path }}"
dest: "{{ item.stat.path }}_{{ ansible_date_time.iso8601_basic_short }}"
mode: "preserve"
remote_src: true
loop: "{{ crontabFile.results }}"
when: item.stat.exists
This works fine so far. Afterwards I check for existing backups:
- name: find backups of crontab
ansible.builtin.find:
paths: "{{ item.stat.path | dirname }}"
patterns: "{{ item.stat.path | basename }}_*"
loop: "{{ crontabFile.results }}"
register: crontabBackup
when: item.stat.exists
The next step would be to loop over the results of crontabBackup and remove the oldest x files of the backups for each user individually.
The following does not work:
- name: delete oldest backups of crontab, keep last 5
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop: "{{ (crontabBackup.results|selectattr('files', 'defined')|map(attribute='files')|flatten|sort(attribute='mtime')|map(attribute='path')|list)[:-5] }}"
What happens is, that a flat list of all files is generated and the oldest files of this list get removed. What I want is to sort the list per user and remove the files per user.
After trying many different approaches, I think I've figured it out for myself. I will share my solution in case anyone is facing the same issue.
The solution is to use an outer loop that includes the inner loop via include_tasks as mentioned here.
# main.yml
- include_tasks: delete_backups_inner_loop.yml
loop: "{{ crontabBackup.results|selectattr('files', 'defined')|map(attribute='files') }}"
loop_control:
loop_var: userLoop
# delete_backups_inner_loop.yml
- name: delete oldest backups of crontab, keep last 5
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ (userLoop|sort(attribute='mtime'))[:-5] }}"

Ansible loop using ansible_hostnames

I'm trying to update a configuration file for a NiFi deployment, the inital deployment configuration needs to include the nodes to allow HTTPS connections to be established between them.
I have an ansible tasks that makes the required structural changes to the configuration files, but I can't seem to get the right details inserted.
- name: Add each host to the authorizers.xml
lineinfile:
path: /opt/nifi/conf/authorizers.xml
line: "<property name=\"Node Identity {{ item }}\">CN={{ item }}, OU=NiFi</property>"
insertafter: <!--accessPolicyProvider Node Identities-->
loop: "{{ query('inventory_hostnames', 'nifi') }}"
This puts the ip addresses for the hosts, and I need to get the ansible_hostname for each node instead.
I've played around with ansible_play_batch and loop: "{{ groups['nifi'] }}" but I'm getting the result, outputting the ip addresses instead of the short hostnames each time.
The short hostnames are not stored in my ansible configuration anywhere, they are (if I understand correctly) determined at run time via the gathering facts process. I'd really like to not have to put the node names into a list variable.
Q: "Get the ansible_hostname for each node"
A: Given the inventory
shell> cat hosts
[nifi]
10.1.0.51
10.1.0.52
The playbook below
- hosts: nifi
tasks:
- debug:
var: ansible_hostname
gives (abridged)
ok: [10.1.0.51] =>
ansible_hostname: test_01
ok: [10.1.0.52] =>
ansible_hostname: test_02
It's possible to iterate the hosts in the group and get ansible_hostname from the hostvars. For example, delegate_to localhost and run_once
- debug:
msg: "{{ hostvars[item].ansible_hostname }}"
loop: "{{ groups.nifi }}"
delegate_to: localhost
run_once: true
gives
ok: [10.1.0.51 -> localhost] => (item=10.1.0.51) =>
msg: test_01
ok: [10.1.0.51 -> localhost] => (item=10.1.0.52) =>
msg: test_02

Can I create dynamic lists within Ansible vars_files?

I have a variables file that includes important info about our databases; the server they are on, the db version, the DB_HOME directory, etc. In the variables file, I would like to dynamically create lists that capture the unique values of those properties, so they can be easily iterated through in a task.
I have equivalent functionality by creating the list on the fly in a task's loop option, but that means repeating that loop syntax (violates DRY principle) and I would like less sophisticated Ansible colleagues to be able to use a pre-defined list.
example of the variables file databases.yml:
databases:
- name: test_db1
server: ora_901
listener: LISTENER_XYZ
version: '11.2.0.4'
oracle_home: '/app/oracle/product/11.2.0.4/db_home'
- name: test_db2
server: ora_902
listener: LISTENER_ABC
version: '11.2.0.4'
oracle_home: '/app/oracle/product/11.2.0.4/db_home'
## This didn't work... was hoping I could build this list dynamically
listeners:
- name: "{{ item }}"
loop: "{{ databases | map(attribute = 'listener') | list | unique }}"
servers:
- name: "{{ item }}"
loop: "{{ databases | map(attribute = 'server') | list | unique }}"
I would then use this loop through either the 'listeners' or 'servers' lists directly with some tasks.
When I tried a task that referenced the listeners variable, it failed. Referencing databases works and all items are returned, so I know it's getting some data from the vars_file...
- vars_files:
- vars/databases.yml
tasks:
- debug:
msg: "{{ databases }}"
- debug:
msg: "{{ listeners }}"
TASK [debug] **************************************************************************
ok: [FQDN] => {
"msg": [
{
"listener": "LISTENER_XYZ",
"name": "test_db1",
"oracle_home": "/app/oracle/product/11.2.0.4/db_home",
"server": "ora_901",
"version": "11.2.0.4"
},
{
"listener": "LISTENER_ABC",
"name": "test_db2",
"oracle_home": "/app/oracle/product/11.2.0.4/dbhome_1",
"server": "ora_902",
"version": "11.2.0.4"
},
fatal: [FQDN]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'item' is undefined\n\nThe error appears to have been in '/home/xxx/test_vars.yml': line 21, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n - debug:\n ^ here\n"}
I would really like to keep these dynamic definitions in the same place as the server definitions, and don't see why it wouldn't be possible, I'm just sure I'm using the wrong mechanism.
Check this out from Ansible doc// Just to give you an idea, about "loop_control".
You can nest two looping tasks using include_tasks. However, by default Ansible sets the loop variable item for each loop. This means the inner, nested loop will overwrite the value of item from the outer loop. You can specify the name of the variable for each loop using loop_var with loop_control:
# main.yml
- include_tasks: inner.yml
loop:
- 1
- 2
- 3
loop_control:
loop_var: outer_item
# inner.yml
- debug:
msg: "outer item={{ outer_item }} inner item={{ item }}"
loop:
- a
- b
- c

Ansible role checking path of dynamic variable within task

I have a couple of ansible role tasks which I am using to setup a number of configuration directories. The problem is that I have a list of configurations which need to be setup with a certain path but only if they do not exist.
- name: Ensure core configuration directories exist.
shell: "cp -r {{ service_install_path }}/conf/ {{ service_home }}/{{ item }}.conf.d"
when:
- "item in conf_indexes_current.content"
- "not {{ service_home }}/{{ item }}.conf.d.stat.exists"
with_items: "{{ conf_dirs }}"
The problem however is that you can't stat a path like this:
- "not {{ service_home }}/{{ item }}.conf.d.stat.exists"
The error I am getting is this:
The conditional check 'not {{ service_home }}/{{ item
}}.conf.d.stat.exists' failed. The error was: unexpected '/'
line 1
Is there a way to set a dynamic variable full path and then test it? It does not seem like I can set a fact here either.
[update]: Just read another question trying to do something with a very vaguely similar loop concept. Is the correct method to simply use a shell script/template at this point?
Alright I did eventually figure this out with hints from bits and pieces of other Stack questions and random sites. I don't use ansible very often and we've only been using it for roughly a year. I ended up doing the following, breaking it into two tasks:
- name: Check existence of config for each index.
stat:
path: "{{ service_home }}/{{ item }}.conf.d"
register: "{{ item }}_conf_true"
with_items: "{{ conf_dirs }}"
- name: Ensure core configuration directories exist as full templates for modification.
shell: "cp -r {{ service_install_path }}/conf/ {{ service_home }}/{{ item }}.conf.d"
when:
- "item in conf_indexes_current.content"
- "{{ item }}_conf_true is not defined"
with_items: "{{ conf_dirs }}"
Does it work? Yes. Is it proper? Maybe but there is likely a better way I have not thought of or seen yet.

How to loop in Ansible $var number of times?

I want to run a loop in Ansible the number of times which is defined in a variable. Is this possible somehow?
Imagine a list of servers and we want to create some numbered files on each server. These values are defined in vars.yml:
server_list:
server1:
name: server1
os: Linux
num_files: 3
server2:
name: server2
os: Linux
num_files: 2
The output I desire is that the files /tmp/1, /tmp/2 and /tmp/3 are created on server1, /tmp/1 and /tmp/2 are created on server2. I have tried to write a playbook using with_nested, with_dict and with_subelements but I can't seem to find any way to to this:
- hosts: "{{ target }}"
tasks:
- name: Load vars
include_vars: vars.yml
- name: Create files
command: touch /tmp/{{ loop_index? }}
with_dict: {{ server_list[target] }}
loop_control:
loop_var: {{ item.value.num_files }}
If I needed to create 50 files on each server I can see how I could do this if I were to have a list variable for each server with 50 items in it list which is simply the numbers 1 to 50, but that would be a self defeating use of Ansible.
There is a chapter in the docs: Looping over Integer Sequences (ver 2.4)
For your task:
- file:
state: touch
path: /tmp/{{ item }}
with_sequence: start=1 end={{ server_list[target].num_files }}
Update: things has changed in Ansible 2.5. See separate docs page for sequence plugin.
New loop syntax is:
- file:
state: touch
path: /tmp/{{ item }}
loop: "{{ query('sequence', 'start=1 end='+(server_list[target].num_files)|string) }}"
Unfortunately sequence accepts only string-formatted parameters, so parameters passing to query looks quite clumsy.

Resources