Synchro
Synchro

Reputation: 37750

Ansible dynamic lookup in a nested list

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?

Upvotes: 0

Views: 1379

Answers (1)

ceving
ceving

Reputation: 23866

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.

Upvotes: 1

Related Questions