Lukas
Lukas

Reputation: 23

Ansible nested loop over host group members and their host_vars

Can someone help me with nested loop construction?

I am creating simple backup role, which will prepare client servers for backup and it will prepare backup server also. Backups will be downloaded from clients by backup server at defined time. I want to define different time and different folders for each client (by host_vars).

My workflow:

  1. backup server

    • install packages
    • create backup user
    • create backup directory
  2. client

    • install packages
    • add backup's server ssh key to authorized_keys (with restrictions)
    • some ssh settings
    • reload sshd
  3. backup server

    • set cron jobs for download backups (each client should has it's own cron entry - different time and different set of backuped folders)

I have first version of this role, which was applied to client group of servers. Backup server was configured with help of delegate_to: statement. But there was problem with creating cron file on backup server, because there was something like 'race condition'. When playbook finishes, there was only one random entry in the backup's server cron file. But I expected that there should be entries for all client servers. I tried to open issue https://github.com/ansible/ansible/issues/74189 - I got answer, that I should change my access to this problem.

My second attempt was that I rewrite ansible role. Then I can apply it on backup server instead of client group. Now I am using delegate_to: on the group of client servers.

Example of (simplified) expected cron job on backup server:

0 0 * * * backup -include /home --include /var/www --include /srv --exclude '**' backup.lab.local::/ /home/backup/srv1.lab.local
# -> similar entry for srv2
# -> similar entry for srv3

My scenario:

# hosts file
[backup_servers]
backup.lab.local

[testing_servers]
srv1.lab.local
srv2.lab.local
srv3.lab.local

Example of host_vars file:

# host_vars/srv1.lab.local
backup_folders:
  - /home
  - /var/www
  - /srv

I am stuck on task which creates cron entries on backup server. I need to loop groups['testing_servers'] and inside this loop I need to create another loop of hostvars[<each_host_from_group>]['backup_folder'].

How to do it, please?

Upvotes: 2

Views: 705

Answers (2)

Halis
Halis

Reputation: 387

Cron module and parallel write

Regarding the mentioned GitHub issue, you can probably go around the issue (which is parallel write to crontab file on one host) by using throttle: 1 (documentation)

i.e. your cron playbook should contain:

  tasks:
    - name: "Set backup cron job"
      ansible.builtin.cron:
        name: "Backup cron job for {{ inventory_hostname }}"
        user: root
        minute: "{{ backup_minute }}"
        hour: "{{ backup_hour }}"
        day: "{{ backup_day }}"
        month: "{{ backup_month }}"
        weekday: "{{ backup_weekday }}"
        job: "rdiff-backup --create-full-path --remote-schema \"ssh -C -i /home/backup/.ssh/id_rsa -p {{ ansible_port | default(22) }} -o 'StrictHostKeyChecking no' \\%s rdiff-backup --server\" --include /path/to/dir --exclude '**' {{ ansible_host }}::/ /home/backup/{{ inventory_hostname }}"
      delegate_to: deb10.lab.local
      throttle: 1                       # <--------- THIS IS THE IMPORTANT LINE

Nested loop

The other way around you are mentioning using nested loop can be done either by using nested loop in Ansible, or by prebuilding nested structure, and then iterate over it:

Ansible - nested loops

Documentation

You can build loop in loop as the documentation says, by using one task as outer loop with include_tasks module, and one loop with renamed loop control variable (loop_var).

As the example in the documentation shows, you need to separate it into two files:

# main.yml tasks file
- include_tasks: inner.yml
  loop:
    - 1
    - 2
    - 3
  loop_control:
    loop_var: outer_item

and

# inner.yml tasks file
- name: Print outer and inner items
  ansible.builtin.debug:
    msg: "outer item={{ outer_item }} inner item={{ item }}"
  loop:
    - a
    - b
    - c

Jinja2 - pregenerate structure

Documentation

Use product filter to create list of lists:

['alice', 'bob'] |product(['clientdb', 'employeedb', 'providerdb'])|list

creates

[('alice', 'clientdb'), ('alice', 'employeedb'), ('alice', 'providerdb'), ('bob', 'clientdb'), ('bob', 'employeedb'), ('bob', 'providerdb')]

so as example in documentation states, you can use code similar to following:

- name: Give users access to multiple databases
  community.mysql.mysql_user:
    name: "{{ item[0] }}"
    priv: "{{ item[1] }}.*:ALL"
    append_privs: yes
    password: "foo"
  loop: "{{ ['alice', 'bob'] |product(['clientdb', 'employeedb', 'providerdb'])|list }}"

Upvotes: 0

β.εηοιτ.βε
β.εηοιτ.βε

Reputation: 39159

I wouldn't loop over backup_folder, I would join those values.

Given that you properly loop over groups['testing_servers'] hosts, e.g.

0 0 * * * backup {{ ([''] + hostvars[item]['backup_folder']) | join(' --include ') }}  --exclude '**' backup.lab.local::/ /home/backup/{{ item }}

Would give:

0 0 * * * backup --include /home --include /var/www --include /srv --exclude '**' backup.lab.local::/ /home/backup/srv1.lab.local

Note that: this odd construct: ([''] + hostvars[item]['backup_folder']) is there to create an empty element at the begining of your list, to make it start with --include, otherwise you will have

... backup /home --include /var/www --include /srv --exclude '**' ...
##        ^--- missing "--include " here

Upvotes: 1

Related Questions