1

How do I write a yaml file such that it defines an array with a list of values, and it skips those values from being added to the yaml array if the given value is undefined?

Specifically, I'm trying to define an array of IP addresses to which I want a service (sshd) to bind to, including the ipv4 and ipv6 addresses. If there is both an ipv4 & ipv6 address, then the array should contain both values. If there is only an ipv4 address or only an ipv6 address, then it should contain one of those values.

This doesn't work, as it will produce an error "msg": "AnsibleUndefinedVariable: 'dict object' has no attribute 'address'"}

    ssh_listen_to:
      - "{{ ansible_default_ipv4.address }}"
      - "{{ ansible_default_ipv6.address }}"

This also doesn't work, as it will end-up adding a line ListenAddress with no argument, that breaks the sshd server (this may be a shortsighted design of my ansible role, but that's out-of-scope in this question).

    ssh_listen_to:
      - "{{ ansible_default_ipv4.address | default('') }}"
      - "{{ ansible_default_ipv6.address | default('') }}"

How can I update the above yaml block such that it will skip adding the values to the array if the value isn't defined? Note that this is being done in my playbook's vars: section, so no tasks-based solutions please.

1
1

Create the list in Jinja2 and convert it, e.g.

- hosts: localhost
  gather_facts: true
  tasks:
    - set_fact:
         ssh_listen_to: "{{ _ssh_listen_to|from_yaml }}"
      vars:
        _ssh_listen_to: |
          {% if ansible_default_ipv4.address is defined %}
          - {{ ansible_default_ipv4.address }}
          {% endif %}
          {% if ansible_default_ipv6.address is defined %}
          - {{ ansible_default_ipv6.address }}
          {% endif %}
    - debug:
        var: ssh_listen_to|type_debug
    - debug:
        var: ssh_listen_to

gives

TASK [debug] ********************************************************************
ok: [localhost] => 
  ssh_listen_to|type_debug: list

TASK [debug] ********************************************************************
ok: [localhost] => 
  ssh_listen_to:
  - 10.1.0.27

If you put the name of the variables into the list, e.g.

    vars_addr:
      - default_ipv4
      - default_ipv6

the task below gives the same result

    - set_fact:
         ssh_listen_to: "{{ vars_addr|
                            map('extract', ansible_facts, 'address')|
                            select('defined')|
                            list  }}"
1

Use select() with the defined test.

ssh_listen_to: "{{ [ansible_facts.default_ipv4.address, ansible_facts.default_ipv6.address] | select('defined') | list }}"

As for the linked post about matching dictionary keys in JMESPath, I wouldn't use JMESPath at all for that in Ansible. As I've grown more accustomed to Jinja I've found json_query() less and less necessary.

ssh_listen_to: "{{ ansible_facts | select('match', '.+_ipv[46]$') | map('extract', ansible_facts, 'address') | select('defined') | list }}"

ansible_facts | select('match', '.+_ipv[46]$') filters the keys using a regex. Iterating over a dictionary returns its keys, which are then matched against the provided regex.

Passing this filtered list of keys to map('extract', ansible_facts, 'address') extracts their values and looks up 'address' in each value. You could do this in two steps as map('extract', ansible_facts) | map(attribute='address'), if that makes things clearer.

select('defined') gets rid of any undefined values, then list converts the generator returned from select() into a list (this generator unrolling happens automatically in ansible-base/ansible-core >= 2.10, but a lot of people are still on Ansible 2.9.)

2
  • wonderful answer, thank you! Would you mind elaborating on that second command? What is 46? – Michael Altfield Mar 18 at 9:09
  • 1
    [46] is a regex character class; it matches a single '4' or '6' in the string. – flowerysong Mar 18 at 14:41
0

This doesn't answer the question directly, but I found that I could query the entire ansible facts dictionary as a json document using the json_query filter and a jmespath expression.

For example, the following produces a list of all the ip addresses.

ssh_listen_to: "{{ ansible_facts | json_query('*.address') }}"

You can make this safer by just taking the default ipv4 & ipv6 addresses like so:

ssh_listen_to: "{{ ansible_facts | json_query('[@.default_ipv4, @.default_ipv6][].address') }}"

And you can even further check the sanity of the options by removing anything from the list that's not an IP address

ssh_listen_to: "{{ ansible_facts | json_query('[@.default_ipv4, @.default_ipv6][].address') | ansible.netcommon.ipaddr }}"
2
  • Be careful shell> export address="This is not what I want." will render the code unusable. – Vladimir Botka Mar 17 at 19:38
  • @VladimirBotka I updated the answer with some sanity checks :) – Michael Altfield Mar 18 at 9:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.