0

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?

Synchro
  • 35,538
  • 15
  • 81
  • 104
  • Why do you define dbservers, if you do not use the `to_ip` parameter of the `ufw` module? – ceving Aug 10 '18 at 15:01
  • Because there may be other servers that need to allow access *from* `dbservers`, such as backup hosts - I'm only showing a tiny subset of my full config. I know that in theory I could get the same info from gathered facts, but there are a lot of variables involved there - for example interface names vary across servers so I can't assume things like `eth0` anywhere, and I'd need to filter out link-local IPv6 addresses. It doesn't make any difference to the Q - I'd still need to do the same kind of lookups, no matter the source. – Synchro Aug 10 '18 at 15:07

1 Answers1

1

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.

ceving
  • 21,900
  • 13
  • 104
  • 178
  • The problem here is that these vars are not set for non-matching hosts - I need to have access to all the IPs from all the hosts - the approach you describe will simply let each host know its own IPs, which isn't very useful. From my example, I want a host like xyz to obtain the IPs for www1 and www2 - the task is not run on the www1 or www2 hosts. – Synchro Aug 10 '18 at 09:57
  • Also I've found that the inventory itself is too cryptic to query to this level of detail, to the point where that's not worth trying. Obtaining facts on out-of-scope targets is also problematic. – Synchro Aug 10 '18 at 09:59
  • Ansible has very limited scope. Almost everything is globally stored in `hostvars`. See [here](https://stackoverflow.com/a/40244734/402322). In most cases it is better to avoid complicated data structures in Ansible. But this does not mean, that is not possible to handle them. You can use Jinja expressions to use any kind of complicated data. See [here](https://stackoverflow.com/a/40629950/402322) for an example, I wrote for the generation of packet filters. Using Jinja in host_vars files, helps you to create suitable structured variables for your firewall or database tasks. – ceving Aug 10 '18 at 10:25
  • There are really two steps: 1: "find all the elements in this array with keys that match keys in that array" (like [`array_intersect_key`](http://php.net/manual/en/function.array-intersect-key.php) in PHP), then 2: "iterate over an array collecting selected sub-element lists from each". I can't find what syntax I should use for even step 1. Delegation doesn't help as I would only know what values to send to delegated hosts *after* I had extracted them. – Synchro Aug 10 '18 at 12:32
  • Thanks for updating. This is great - I've never even *seen* the `{%` syntax in the docs! I don't see how this restricts the IP list to only matching source hosts, e.g. so my dbservers play might want only webservers, and not include themselves? To get over the cross-product thing you mention, my aim is to do this within a role task so that it is run & resolved per-host (e.g. on each dbserver), and not per-group. I'm still finding the play/role divide a little tricky; so far I've been putting everything in roles as I find they make more sense to me. – Synchro Aug 10 '18 at 15:21
  • Check the [Jinja2 docs here](http://jinja.pocoo.org/docs/2.10/templates/) - lots of useful and interesting tidbits like the `{%...%}` syntax. – Paul Hodges Aug 10 '18 at 16:01