Loop with conditional in an Ansible template

Viewed 115

I'm attempting to write information to a file from a registered variable in my task. It's basically a port test and for the life of me I cannot figure out how to only list items that failed.

Here is what I have:

Task:

- name: Check if ports are open
  wait_for:
    host: "{{ item.name }}"
    port: "{{ item.port }}"
    state: started
    delay: 0
    timeout: 5
  ignore_errors: true
  loop: "{{ server_facts.server_port|default([]) }} + {{ server_port | default([]) }}"
  register: server_port_check

Here is the content for the registered variable server_port_check:

server_port_check:
  changed: false
  failed: true
  msg: All items completed
  results:
  - ansible_loop_var: item
    changed: false
    elapsed: 0
    failed: false
    invocation:
      module_args:
        active_connection_states:
        - ESTABLISHED
        - FIN_WAIT1
        - FIN_WAIT2
        - SYN_RECV
        - SYN_SENT
        - TIME_WAIT
        connect_timeout: 5
        delay: 0
        exclude_hosts: null
        host: dc.com
        msg: null
        path: null
        port: 636
        search_regex: null
        sleep: 1
        state: started
        timeout: 5
    item:
      name: dc.com
      port: 636
    match_groupdict: {}
    match_groups: []
    path: null
    port: 636
    search_regex: null
    state: started
  - ansible_loop_var: item
    changed: false
    elapsed: 6
    failed: true
    invocation:
      module_args:
        active_connection_states:
        - ESTABLISHED
        - FIN_WAIT1
        - FIN_WAIT2
        - SYN_RECV
        - SYN_SENT
        - TIME_WAIT
        connect_timeout: 5
        delay: 0
        exclude_hosts: null
        host: salt
        msg: null
        path: null
        port: 4505
        search_regex: null
        sleep: 1
        state: started
        timeout: 5
    item:
      name: salt
      port: 4505
    msg: Timeout when waiting for salt:4505

Then in my template file, I have the following

Port Checks:
{% if server_port_check.failed == true %}
The following ports are not open:
{% for result in server_port_check.results %}
Host:   {{ result.item.name }}
Port:   {{ result.item.port }}
{% endfor %}
{% endif %}

{% if server_port_check.failed == false %}
The following ports are open:
{% for result in server_port_check.results %}
Host:   {{ result.item.name }}
Port:   {{ result.item.port }}
{% endfor %}
{% endif %}

This ends up with the following results:

Port Checks:
The following ports are not open:
Host:   dc.com
Port:   636
Host:   salt
Port:   4505

Which is incorrect since the port for dc.com is open according to the job output.

2 Answers

If you look at your ouput carefully, you will realise that you have a failed key at the task level (the one you are using already), but also at the results. This is something you want to take into account in your loops.

Knowing that, you will first have to get rid of your two conditions, as, the failed status of the task itself does not presuppose the status of the items of the task. It just gives you a general indication that there is at least one elements that failed.

Then, you can either do a {% for ... if ... %}

{% for result in server_port_check.results if result.failed %}
{% for result in server_port_check.results if not result.failed %}

Or a filter with the sélect filter along with the failed and success tests

{% for result in server_port_check.results | select("failed") %}
{% for result in server_port_check.results | select("success") %}

With your data, the following template:

Port Checks:
{% for result in server_port_check.results | select("failed") %}
  {%- if loop.first -%}
    The following ports are not open:
  {%- endif %}

Host:   {{ result.item.name }}
Port:   {{ result.item.port }}
{% endfor %}

{% for result in server_port_check.results | select("success") %}
  {%- if loop.first -%}
    The following ports are open:
  {%- endif %}

Host:   {{ result.item.name }}
Port:   {{ result.item.port }}
{% endfor %}

Will render in:

Port Checks:
The following ports are not open:
Host:   salt
Port:   4505
  
The following ports are open:
Host:   dc.com
Port:   636

Since you are looping, server_port_check.failed is the overall result from all the loops in your task. It will be true (i.e. failed) even if only one port failed the check in your list. So you still need to check each individual result for inclusion in your report list or not. What I just explained is also true for success results.

Below is how I would try to fix your problem. Since there are a few bad practices in your example I fixed them as well.

  • comparing to bare boolean should be replaced by testing the value with the boolean filter:
    • a == true becomes a | bool
    • a == false becomes not b | bool
  • moreover checking for task failure has dedicated failed / success tests which are much more appropriate.

Rather than using the global failure result, I declared two vars containing the failed and success results by filtering the raw one then testing on their length. I also used the zip filter to create an overall iteration on failed/success ports and lower the amount of code in the template.

Port Checks:
{% set failed_ports = server_port_check.results | select('failed') %}
{% set success_ports = server_port_check.results | select('success') %}

{% for port_type in ([failed_ports, success_ports] | zip(['not open', 'open'])) %}
{% if port_type.0 | length > 0 %}
The following ports are {{ port_type.1 }}:
{% for result in port_type.0 %}
Host:   {{ result.item.name }}
Port:   {{ result.item.port }}
{% endfor %}
{% endif %}
{% endfor %}

Related