Hi,
I’m slowly but steadily adopting Ansible for real work. It’s a slippery fish, but once you get the hang of it, you won’t miss it.
Right, here goes. I’ve written a playbook to manage my (local and public) backup servers using Rocky Linux and Rsnapshot. To write the rsnapshot.conf
configuration file, I used a Jinja2 template file. Here’s a snippet from the file:
#######################
# BACKUP TARGET HOSTS #
#######################
{% for backup_target in backup_targets %}
# {{ backup_target }}
{% for backup_dir in backup_dirs %}
backup root@{{ backup_target }}:{{ backup_dir}} {{ backup_target }}
{% endfor %}
{% endfor %}
The corresponding variables are in the host_vars
section and look like this:
backup_targets:
- alphamule.microlinux.lan
- gustave.microlinux.lan
- proxy.microlinux.lan
backup_dirs:
- /etc
- /home
With all this, here’s what the resulting configuration file on the target looks like:
#######################
# BACKUP TARGET HOSTS #
#######################
# alphamule.microlinux.lan
backup root@alphamule.microlinux.lan:/etc alphamule.microlinux.lan
backup root@alphamule.microlinux.lan:/home alphamule.microlinux.lan
# gustave.microlinux.lan
backup root@gustave.microlinux.lan:/etc gustave.microlinux.lan
backup root@gustave.microlinux.lan:/home gustave.microlinux.lan
# proxy.microlinux.lan
backup root@proxy.microlinux.lan:/etc proxy.microlinux.lan
backup root@proxy.microlinux.lan:/home proxy.microlinux.lan
Now here comes the tricky part. For now I’m assuming that Rsnapshot does backups of the same directory trees on every host, e. g. /etc
and /home
in the example above.
I’m now encountering a situation where I have different backup_dirs
for different backup_targets
. Something like this:
- alphamule.microlinux.lan:
- /etc
- /home
- gustave.microlinux.lan:
- /home
- proxy.microlinux.lan:
- /etc
- /var
- /srv
How can I make Jinja2 loop over this list of lists? It looks like I’m missing some IQ points to translate this into correct Jinja2 syntax. So I thought I’d ask the usual suspects among the gurus in this forum for a little help.
Cheers,
Niki
Make it a list of dictionaries first and then loop over it.
backup_targets:
- hostname1:
- /etc
- /home
- hostname2:
- /etc
## example template
{% for item in backup_targets %}
{% for data in item|dict2items %}
target: {{ data.key }}
# join the list
dirs: {{ data.value|join(', ') }}
# or alternatively, loop the list
{% for dir in data.value %}
{{ dir }}
{% endfor %}
{% endfor %}
{% endfor %}
## generates the below
target: hostname1
# join the list
dirs: /etc, /home
# or alternatively, loop the list
/etc
/home
target: hostname2
# join the list
dirs: /etc
# or alternatively, loop the list
/etc
## another example template
{% for item in backup_targets %}
{% for k,v in item.items() %}
{{ k }} {{ v|join(', ') }}
{% endfor %}
{% endfor %}
## generates the below
hostname1 /etc, /home
hostname2 /etc
1 Like
The backup_targets is a list of dictionaries. Took me a while to figure that out. An alternative is to have bit different dictionaries in the list:
vars:
- backup_targets:
- alphamule.microlinux.lan:
- /etc
- /home
- gustave.microlinux.lan:
- /home
- other:
- host: alphamule.microlinux.lan
path: /etc
- host: alphamule.microlinux.lan
path: /home
- host: gustave.microlinux.lan
path: /home
{% for backup_target in backup_targets %}
{% for key, value in backup_target.items() %}
{% for dir in value %}
# {{ key }}:{{ dir }} {{ dir }}
{% endfor %}
{% endfor %}
{% endfor %}
{% for backup_target in other %}
# {{ backup_target.host }}:{{ backup_target.path }} {{ backup_target.path }}
{% endfor %}
Thank you very much for your detailed reply. I’m experimenting with it and trying to adapt it to my play, and I’m slowly getting somewhere.
I have a little follow-up question to it. My initial variable definition looked like this:
backup_targets:
- alphamule.microlinux.lan
- gustave.microlinux.lan
- proxy.microlinux.lan
Now the simple backup_targets
list was used in other parts of the playbook, here for example:
- name: Gather SSH public keys from backup target hosts
ansible.builtin.known_hosts:
name: "{{ item }}"
state: present
key: "{{ lookup('pipe', 'ssh-keyscan -t rsa {{ item }}') }}"
with_items:
- "{{ backup_targets }}"
The new variable definition looks like this:
backup_targets:
- sandbox.microlinux.lan:
- /etc
- /home
- squidbox.microlinux.lan:
- /etc/
- /var
- sd-155842.dedibox.fr:
- /etc
- /var
- /root
- /srv
Now this part doesn’t work anymore as it did before:
with_items:
- "{{ backup_targets }}"
Question: how can I rewrite this so I can “extract” (sort of) the simple list of my backup target hosts and the play works again?
One option is to use dict2items
and a bit of creativity. Another option is to use .keys()|first
.
- name: Gather SSH public keys from backup target hosts
ansible.builtin.known_hosts:
name: "{{ (item|dict2items).0.key }}"
state: present
key: "{{ lookup('pipe', 'ssh-keyscan -t rsa {{ (item|dict2items).0.key }}') }}"
with_items:
- "{{ backup_targets }}"
- name: Gather SSH public keys from backup target hosts
ansible.builtin.known_hosts:
name: "{{ item.keys()|first }}"
state: present
key: "{{ lookup('pipe', 'ssh-keyscan -t rsa {{ item.keys()|first }}') }}"
with_items:
- "{{ backup_targets }}"
Debug example of the second option.
backup_targets:
- hostname1:
- /etc
- /home
- hostname2:
- /etc
## example task
- name: "items"
ansible.builtin.debug:
msg: "{{ item.keys()|first }}"
loop: "{{ backup_targets }}"
## produces...
ok: [localhost] => (item={'hostname1': ['/etc', '/home']}) => {
"msg": "hostname1"
}
ok: [localhost] => (item={'hostname2': ['/etc']}) => {
"msg": "hostname2"
}
1 Like
Here’s to everybody in this thread (with a special mention to Louis): a big warm thank you for your detailed answers. I did some experimenting using your various suggestions, and now my configuration works perfectly as expected.
You guys are the greatest.
On a side note, I’d really like to get a firm handle on these things. I wonder if this is the time to (finally) start to learn Python. I’m hesitating since I don’t plan on becoming a developer. Would this be overkill, a bit like studying food chemistry when all you want to do is cook a few good dishes with a selection of good recipes? Or is it worth it?
Cheers,
Niki
Ah, that does allow:
{% for backup_target in backup_targets %}
{% for dir in backup_target.values()|first %}
# {{ backup_target.keys()|first }}:{{ dir }}
{% endfor %}
{% endfor %}
That was what I was stumbling with too.
backup_targets:
- alphamule
- gustave
is a list of values, while
backup_targets:
- sandbox: something
- squidbox: other
is a list of dictionaries, just like the
backup_targets:
- host: sandbox
value: something
- host: squidbox
value: other
I don’t know much of Python. Yes, it would probably save time to know some elementary things about it, like that dict objects have keys() and values() … and to know a dict is a dict when one sees it.