Lior
Lior

Reputation: 13

Iterating over complex lists in Ansible

I have this list of firwalld zones to configure:

fw_zones:
  - name: "example"
    target: "DROP"
    allowed_services:
      - ssh
    icmp_block_inversion: true
    icmp_blocks:
      - fragmentation-needed
  - name: "another_example"
    target: "DROP"
    allowed_services:
      - ssh
      - http
      - ntp
    icmp_block_inversion: true
    icmp_blocks:
      - fragmentation-needed
      - echo-request

and I want to use the firewalld module to do so. While setting new zones shouldn't be a problem, and setting their default target can probably only be done using command module, I am struggling to find a solution for enabling services/icmp blocks since I'm already iterating over a list (fw_zones). Is there a straightforward solution to achieving this?

I tried different setups of this list, like:

fw_zones:
  example:
    target: "DROP"
    allowed_services:
      - ssh
    icmp_block_inversion: true
    icmp_blocks:
      - fragmentation-needed
  another_example:
    target: "DROP"
    allowed_services:
      - ssh
      - http
      - ntp
    icmp_block_inversion: true
    icmp_blocks:
      - fragmentation-needed
      - echo-request

and using | dict2items, but in all cases I couldn't figure out how to iterate over the icmp_blocks and allowed_services lists.

Upvotes: 1

Views: 60

Answers (2)

Alexander Pletnev
Alexander Pletnev

Reputation: 3516

What you're looking for is nested loops.

While the recommendation is to construct the data in a way that avoids nested loops, it's obvious that sometimes it will make the maintenance of the data complicated, like in your case.

So, you can either run the loop over some product of your list, or use the nested include_tasks. In my opinion, the difference would be that using product could be more elegant, but nested tasks are more straightforward.

So, given your initial list, you can do something like the following:

# playbook.yaml
---
- name: Manage the firewalld settings
  hosts: all
  tasks:
    - name: Looping over the zones
      include_tasks: firewalld_zones_loop.yaml
      loop: "{{ fw_zones }}"
      loop_control:
        loop_var: fw_zone
# firewalld_zones_loop.yaml
---
- name: Looping over the services
  include_tasks: firewalld_services_loop.yaml
  loop: "{{ fw_zone.allowed_services }}"
  loop_control:
    loop_var: fw_service
# firewalld_services_loop.yaml
---
- name: Set the firewall rule
  firewalld:
    zone: "{{ fw_zone.name }}"
    target: "{{ fw_zone.target }}"
    service: "{{ fw_service }}"
    icmp_block: "{{ fw_icmp_block }}"
    state: enabled
  # ...
  loop_control:
    loop_var: fw_icmp_block

Upvotes: 1

Vladimir Botka
Vladimir Botka

Reputation: 68189

Use the filter subelements or the lookup plugin subelements. I prefer the filter, but the difference is only in the syntax. For example,

  • given the list
    fw_zones:
      - name: example
        target: DROP
        allowed_services:
          - ssh
        icmp_block_inversion: true
        icmp_blocks:
          - fragmentation-needed
      - name: another_example
        target: DROP
        allowed_services:
          - ssh
          - http
          - ntp
        icmp_block_inversion: true
        icmp_blocks:
          - fragmentation-needed
          - echo-request

iterate allowed_services

    - name: Iterate allowed_services
      debug:
        msg: "{{ item.0.name }} {{ item.1 }}"
      loop: "{{ fw_zones | subelements('allowed_services') }}"
      loop_control:
        label: "{{ item.0.name }} {{ item.1 }}"

gives (abridged)

    msg: example ssh
    msg: another_example ssh
    msg: another_example http
    msg: another_example ntp

Iterate icmp_blocks

    - name: Iterate icmp_blocks
      debug:
        msg: "{{ item.0.name }} {{ item.1 }}"
      loop: "{{ fw_zones | subelements('icmp_blocks') }}"
      loop_control:
        label: "{{ item.0.name }} {{ item.1 }}"

gives (abridged)

    msg: example fragmentation-needed
    msg: another_example fragmentation-needed
    msg: another_example echo-request

  • You can also use a dictionary
    fw_zones:
      example:
        target: DROP
        allowed_services:
          - ssh
        icmp_block_inversion: true
        icmp_blocks:
          - fragmentation-needed
      another_example:
        target: DROP
        allowed_services:
          - ssh
          - http
          - ntp
        icmp_block_inversion: true
        icmp_blocks:
          - fragmentation-needed
          - echo-request

In this case, use the filter dict2items to convert the dictionary to a list and use the key/value attributes. The below tasks give the same results

    - name: Iterate allowed_services
      debug:
        msg: "{{ item.0.key }} {{ item.1 }}"
      loop: "{{ fw_zones | dict2items | subelements('value.allowed_services') }}"
      loop_control:
        label: "{{ item.0.key }} {{ item.1 }}"

    - name: Iterate icmp_blocks
      debug:
        msg: "{{ item.0.key }} {{ item.1 }}"
      loop: "{{ fw_zones | dict2items | subelements('value.icmp_blocks') }}"
      loop_control:
        label: "{{ item.0.key }} {{ item.1 }}"

Example of a complete playbook for testing

- hosts: localhost

  vars:

    fw_zones:
      example:
        target: DROP
        allowed_services:
          - ssh
        icmp_block_inversion: true
        icmp_blocks:
          - fragmentation-needed
      another_example:
        target: DROP
        allowed_services:
          - ssh
          - http
          - ntp
        icmp_block_inversion: true
        icmp_blocks:
          - fragmentation-needed
          - echo-request

  tasks:

    - name: Iterate allowed_services
      debug:
        msg: "{{ item.0.key }} {{ item.1 }}"
      loop: "{{ fw_zones | dict2items | subelements('value.allowed_services') }}"
      loop_control:
        label: "{{ item.0.key }} {{ item.1 }}"

    - name: Iterate icmp_blocks
      debug:
        msg: "{{ item.0.key }} {{ item.1 }}"
      loop: "{{ fw_zones | dict2items | subelements('value.icmp_blocks') }}"
      loop_control:
        label: "{{ item.0.key }} {{ item.1 }}"

Upvotes: 1

Related Questions