Ansible dynamic lookup in a nested list - ansible

I have a static variable that holds IP addresses of all hosts in my inventory (how to obtain this dynamically is a separate question) like this:
server_ips:
www1.example.com:
ipv4:
- 192.168.0.10
- 192.168.0.11
ipv6:
- '2a00:abcd:1234::100'
- '2a00:abcd:1234::101'
www2.example.com:
ipv4:
ipv6:
- '2a00:abcd:1234::200'
- '2a00:abcd:1234::201'
db1.example.com:
ipv4:
- 192.168.1.2
ipv6:
These names align with hosts in my inventory:
[webservers]
www1.example.com
www2.example.com
[dbservers]
db1.example.com
On a task that's run on the dbservers group, I need a list of all IPs from the webserver group (this makes querying facts directly tricky as facts may not have been gathered for those hosts) - in this case it would need to extract:
- 192.168.0.10
- 192.168.0.11
- '2a00:abcd:1234::100'
- '2a00:abcd:1234::101'
- '2a00:abcd:1234::200'
- '2a00:abcd:1234::201'
The tasks will do things like configure firewall and DB access, along the lines of:
- name: Allow web server access to DB server
ufw:
rule: allow
name: mysql
from_ip: "{{ item }}"
loop: "{{ <loop expression goes here> }}"
It's what's in the loop expression that I'm having trouble with.
There are two parts to the query: extract the list of hosts, and then gather the ip addresses - doing it separately for ipv4 and ipv6 is fine.
I can get part way there with expressions like:
{{ server_ips | map('extract', groups['webservers']) }}
{{ server_ips | intersect(groups['webservers']) }}
However, both of these appear to flatten the result so though they find the right items, the ipv4 and ipv6 list elements are not there, so I can't proceed to the next step. Swapping the lists in these didn't help either.
The subelements lookup seems a good way to get the IPs parts (though I actually need sub-subelements) and skip empty entries, but I can't see how to get to that point.
How should I do this lookup?

You try to reinvent functionality, Ansible provides already. You define your DIY inventory although Ansible has already an inventory. And you define your DIY inventory iteration although Ansible knows how to iterate over its inventory.
If you want to assign data to individual hosts use the host_vars directory as shown in the Best Practices.
host_vars/www1.example.com.yml:
ipv4:
- 192.168.0.10
- 192.168.0.11
ipv6:
- '2a00:abcd:1234::100'
- '2a00:abcd:1234::101'
host_vars/www2.example.com.yml:
ipv4:
ipv6:
- '2a00:abcd:1234::200'
- '2a00:abcd:1234::201'
Then you define a task for each host and use the {{ipv4}} or {{ipv6}} lists for anything you want to do.
If you need to execute actions on a different host like a firewall, use Ansible's delegation.
This extracts all IP addresses from your server_ips dictionary:
- hosts: localhost
connection: local
gather_facts: no
vars:
server_ips:
www1.example.com:
ipv4:
- 192.168.0.10
- 192.168.0.11
ipv6:
- '2a00:abcd:1234::100'
- '2a00:abcd:1234::101'
www2.example.com:
ipv4:
ipv6:
- '2a00:abcd:1234::200'
- '2a00:abcd:1234::201'
xyz.example.com:
ipv4:
- 192.168.1.2
ipv6:
ipv4: >-
{% set ipv4 = [] -%}
{% for ips in server_ips.values() | selectattr ('ipv4') | map (attribute='ipv4') | list -%}
{% for ip in ips -%}
{% set _ = ipv4.append(ip) -%}
{% endfor -%}
{% endfor -%}
{{ ipv4 }}
ipv6: >-
{% set ipv6 = [] -%}
{% for ips in server_ips.values() | selectattr ('ipv6') | map (attribute='ipv6') | list -%}
{% for ip in ips -%}
{% set _ = ipv6.append(ip) -%}
{% endfor -%}
{% endfor -%}
{{ ipv6 }}
ips: >-
{{ ipv4 + ipv6 }}
tasks:
- debug: var=server_ips
- debug: var=ipv4
- debug: var=ipv6
- debug: var=ips
But in order to build firewall rules you have to build a cross product. You have to iterate for each destination over all sources to get all rules.

Related

Jinja2 & Ansible - Loop over lines in several text files

I'm using Ansible to retrieve a list of website we host over 50+ servers. I'm using a SQL query to store in a text file named as hostname_websites.txt using Ansible.
The next step is to build a prometheus (blackbox exporter) configuration file using that data.
Each line of each text file represent an URL. For the sake of representation I'll declare that the URL is $URL and the hostname is $HOSTNAME.
I need some jinja2 magic to generate a single yaml file as :
{% for site in *** each text file *** %}
- targets: ['{{ $URL }}']
labels: {'node': '{{ $HOSTNAME }}'}
{% endfor %}
Given that in the end I need to have a config line containing each time the URL which is a line in text file, and the hostname as a label which is in the txt filename.
Every hostname is in a group called production in Ansible, so I tried looping over that group using jinja2 as follow:
{% for host in group['production'] %}
{{ lookup('file', '{{ host }}_websites.txt).splitlines()' }}
{% endfor %}
That gave me some jinja2 parsing error, as if it wasn't rendering the for loop at all.
I stopped here, in the spirit that I would build by configuration skel around this statement.
There are two problems with your template that jump out immediately:
There is no variable group in Ansible (unless you've created one explicitly); you probably want groups.
You never nest Jinja {{...}} markers. If you want to generate a string containing a variable value, you can use string concatenation:
lookup('file', host ~ '_websites.txt')
Or string formatting:
lookup('file', '%s_websites.txt' % host)
Assuming that our inventory has a host node0 in the group production and that there exists a file node0_websites.txt with the following content:
http://stackoverflow.com/
http://ansible.com/
http://stackexchange.com/
Then running this playbook:
- hosts: localhost
gather_facts: false
tasks:
- copy:
dest: output.txt
content: |
{% for host in groups['production'] %}
# Host: {{ host }}
{% for url in lookup('file', host ~ '_websites.txt').splitlines() %}
- {{ url }}
{% endfor %}
{% endfor %}
Generates the following content in output.txt:
# Host: node0
- http://stackoverflow.com/
- http://ansible.com/
- http://stackexchange.com/

Ansible: How to parse string to evaluate jinja expressions during set_fact

I have a string containing a jinja expression. How can I parse/evaluate/expand/ that string so that the expression is replaced the host variable running the role/task?
Detailed explanation:
I do have a proxmox cluster with multiple hosts. I want to manage their iptables with a dedicated role, but with separate data sources. The role should collect all variables that start with a specific prefix, collect their rules for filter and nat tables, sort them and write them into /etc/iptables/rules.v4via ansible.builtin.template.
Later I want to define multiple lxc/ and VMs inside group_vars, e.g. inventory/group_vars/proxmox/lxc_nginx.yml: ```
nginx_reverse_proxy:
host: '<some_cluster_node>'
network:
ip: 10.1.0.3
cidr: 24
iprules_nginx_reverse_proxy:
nat:
weight: 20
entries: |
-A PREROUTING -i {{ network_external_ip.external_device }} -p tcp --dport 80 -j DNAT --to-destination 10.1.0.3
with network_external_ip beeing defined for each node, in inventory/host_vars/some_cluster_node.yml:
network_external_ip:
ip: 1.1.1.1
gateway: 0.0.0.0
mask: 255.255.252.0
cidr: 22
external_device: ens3 # <<< this differs for each cluster node
now I collect all iprules, which might have been defined in multiple files, within my role and try to get them all converted into a sorted dict:
- name: basics/iptables | collect all rules
set_fact:
helper: |
{%- set filter = [] %}
{%- set nat = [] %}
{%- for entry in vars.items() %}
{%- if entry[0].startswith('iprules_') %}
{%- if entry[1].filter is defined %}
{%- set _ = filter.append(entry[1].filter) %}
{%- endif %}
{%- if entry[1].nat is defined %}
{%- set _ = nat.append(entry[1].nat) %}
{%- endif %}
{%- endif %}
{%- endfor %}
{%- set filter = filter | sort(attribute='weight') | map(attribute='entries') | join('\\n') %}
{%- set nat = nat | sort(attribute='weight') | map(attribute='entries') | join('\\n') -%}
filter: "{{ filter }}"
nat: "{{ nat }}"
- name: basics/iptables | create iptables dict
set_fact:
iptables_rules: "{{ helper | from_yaml }}"
- name: debug
debug:
var: iptables_rules
- name: basics/iptables | write ip rules
ansible.builtin.template:
src: rules.v4.j2
dest: /etc/iptables/rules.v4
which results in:
TASK [basics/iptables : basics/iptables | collect all rules] *****************************************************************************************************************************************************************************************************
ok: [<some_cluster_node>]
TASK [basics/iptables : basics/iptables | create iptables dict] **************************************************************************************************************************************************************************************************
ok: [<some_cluster_node>]
TASK [basics/iptables : debug] ***********************************************************************************************************************************************************************************************************************************
ok: [<some_cluster_node>] =>
iptables_rules:
filter: ''
nat: '-A PREROUTING -i {{ network_external_ip.external_device }} -p tcp --dport 80 -j DNAT --to-destination 10.1.0.3 '
Meaning, {{ network_external_ip.external_device }} never gets evaluated to "ens3". It stays the same, as in the inventory. How can I get this expression being parsed?
Information: The shown dicts here are WIP and do not fulfill certain criteria (like I would write all iprules_* into all hosts, regardless if they even host the lxc), but without the basics working this is no a concern right now.
PS: ansible version used
~ $ ansible --version
ansible [core 2.13.5]
config file = /home/apit/.ansible.cfg
configured module search path = ['/home/apit/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3.10/site-packages/ansible
ansible collection location = /home/apit/.ansible/collections:/usr/share/ansible/collections
executable location = /usr/lib/python-exec/python3.10/ansible
python version = 3.10.8 (main, Nov 27 2022, 20:55:16) [GCC 11.3.0]
jinja version = 3.1.2
libyaml = True

Printing ip addresses in a loop

I'm trying to put together a line with the ip addresses of my cluster in a loop:
set_fact: nodelist={%for host in groups['proxmox-cluster']%}"{{hostvars[host].ansible_all_ipv4_addresses|string}}"{% if not loop.last %},{% endif %}{% endfor %}
But when I try to put this line into a file with
lineinfile:
path: "/etc/security/access.conf"
line: "+:root:{{ nodelist }}"
I get:
"[u'192.168.31.238']","[u'192.168.31.248']","[u'192.168.31.252']","[u'192.168.31.250', u'192.168.1.250', u'192.168.32.250', u'192.168.33.250']"
instead of
192.168.31.238,192.168.31.248,192.168.31.252,192.168.31.250,192.168.1.250,192.168.32.250,192.168.33.250
The reason for this is that hostvars[host].ansible_all_ipv4_addresses is an array. That is why you're printing a joined list of strings inside arrays "[u'192.168.31.238']","[u'192.168.31.248']"...
If you only want to print the default ipv4 addresses you can replace your hostvars expression with:
hostvars[host].ansible_default_ipv4.address
If you want to print the first address in the total list of addresses you can replace your hostvars expression with:
hostvars[host].ansible_all_ipv4_addresses | first
If you want to include all ip addresses from all hosts you will need two loops. This can either be done with Jinja2 template syntax, clever filter chains or both. For example replacing the entire expression with a combination of a jinja2 loop with a filter will give you:
{% for host in groups['proxmox-cluster'] %}"{{ hostvars[host].ansible_all_ipv4_addresses | join(',') }}"{% if not loop.last %},{% endif %}{% endfor %}
Personally I try to avoid mixing Jinja2 template syntax inside ansible tasks/playbooks. There is usually a cleaner way using just filter chains. In your case for example you might do something like this:
- name: Print ips in access.conf
lineinfile:
path: /etc/security/access.conf
line: "{{ groups['proxmox-cluster']
| map('extract', hostvars, 'ansible_default_ipv4')
| map(attribute='address')
| join(',') }}"
Try simpler version below
- set_fact:
nodelist: "{{ nodelist|default([]) + hostvars[item].ansible_all_ipv4_addresses }}"
loop: "{{ groups['proxmox-cluster'] }}"
Try with a filter on nodelist variable in lineinfile module:
lineinfile:
path: "/etc/security/access.conf"
line: "{{ nodelist | join(',') }}"

Run an imported Ansible playbook for each unique value in a set of host vars

I've got a playbook which needs to run against my entire inventory, with a list of hostnames as an extra variable (target_hosts).
The hosts in target_hosts all have a group_id hostvar defined on them. I use the whole inventory because some ancillary hosts which correspond to the group_id var need per-group configuration to match in one section.
There will often be multiple group_id values associated with the hosts in the target_hosts list. I need to select the correct inventory group of ancillary hosts and import/run a playbook to configure both sets of servers partway through the main playbook.
This is what I currently do:
include_playbook: group-configure.yaml
vars:
src_hosts: "group-{{ group_id }}-ancillary-1"
dest_hosts: "{{ target_hosts }}"
I currently have to manually separate the target_hosts by group_id manually, then run the main playbook once for each. This has tons of unnecessary overhead.
What I really want to execute is this:
for each group of hosts from `target_hosts` with the same `group_id` hostvar:
import and run group-configure.yaml with:
src_hosts: "ancillary-{{ group_id }}"
target_hosts: restricted to those with that value of `group_id`'
How can I do this? If the current way this is structured won't work, what's the best alternative approach?
I am pretty sure the add_host: combined with groupby is what you are looking for, which will allow you to roll up those hosts by their attribute, and then run the playbook against them as if that group was defined already:
- hosts: localhost
connection: local
gather_facts: no
become: no
vars:
list_of_name_groups: >-
{%- set results = [] -%}
{%- for g_id, items in (dict(hostvars) | dict2items | groupby("value.group_id")) -%}
{%- for hostname in (items | map(attribute="key") | list) -%}
{%- set _ = results.append({"group_id": g_id, "hostname": hostname}) -%}
{%- endfor -%}
{%- endfor -%}
{{ results }}
tasks:
- add_host:
name: '{{ item.hostname }}'
groups: ancillary-{{ item.group_id }}
with_items: '{{ list_of_name_groups }}'
- hosts: ancillary-my-awesome-groupid
# etc etc

Get Number from hostname in Ansible

In Puppet I can extract the number of the Hostname with this example:
$host_number = regsubst($hostname, '^\w+(\d\d)', '\1')
Is there something similar in Ansible?
e.g.:
fqdn: test01.whatever
hostname: test01
output -> newvariable: 01
I want to extract only the number out of the Hostname, so I can use it in my Playbook as a variable.
This will fetch the inventory_hostname and replace any character text with nothing, leaving the numeric text. Obviously, you can use your imagination to apply whatever regex needed.
Ansible playbook:
vars:
host_names:
- {{ inventory_hostname | regex_replace('[^0-9]','') }}
Jinja2 Template:
{% for host in groups['some_group_in_inventory'] %}
Some description of the host where I needed the Number {{ host | regex_replace('[^0-9]','') }}
{% endfor %}
Theres multiple ways of doing the same thing...
You can use ansible_facts, ansible_hostname, shell commands, etc...
---
- hosts: localhost
gather_facts: yes
tasks:
- debug: var=ansible_facts.hostname
- debug: var=ansible_hostname
As raVan96 said, you need to explain better what you need and what you expect...
If you need to extract only "test01" from hostname, then you can use ansible build in filter "regex_replace". Here is my example:
{{ ansible_hostname | regex_replace('^([a-zA-Z_]+)(\d+)$','\\2') | int }}
Then you get: 1
If you need "01", then delete part to pass to integer - "int"

Resources