I am writing a playbook to set up a web application. Before I make any changes to the system I would like to validate that a hostname provided by passing a variable for the machine actually resolves to one of the IP addresses from ansible facts. If this isn't given the certificate generation later on will fail.
I am planning to get the results from community.general.dig and compare the A and AAAA keys in the result dictionary with IP addresses from the ansible facts all_ipv4_addresses/all_ipv6_addresses variables.
When one or a list of IP is in both dictionaries I want to continue with the playbook, else fail with a warning. These IPs I would like to reuse later to aquire the certificate.
I have looked into complex data manipulation but I cannot make proper sense of how to chain the filters to get my desired result. I also am struggling with the fact i have to compare multiple lists or dictionaries with each other.
My original approach was to use the fail builtin and loop over both lists from DNSpython and ansible facts to check for a match. This is insufficient as I need to reuse the list of matching IPs later. The comparison with the ansible_facts.default_ipv4.address
does not seem sufficient since they might have multiple network interfaces (ipv4 and 6) that might not be the default route.
The thought process I had in mind to validate if a certificate could be obtained should be to get all available IPs from all_ipv4_addresses
/ all_ipv6_addresses
and then store the intersection of these records with the valid A/AAAA records from resolving the app_host
variable.
I have found the intersect() filter and now I am just trying to put the entire thing in a condition. Thanks @U880D for pointing me in the right direction towards ansible_facts
, whose snipped i have reused for the start of my playbook:
- name: set up all app services on the target machine
hosts: all
gather_facts: true
gather_subset:
- "!all"
- "!min"
- "network" # in order to get the actual Remote Node configuration
become: yes
vars:
# always overwrite this with the previously setup FQDN
- app_host:
- app_vhost_webroot: /var/www/{{ app_host }}/public
# no trailing / leading slash ; can be an subdirectory like 'app/wws' relative to webroot
- app_path: wws
- app_db: app
- app_dbhost: localhost
- app_dbuser: app
- app_dbpassword: lookup('community.general.random_string', length=14, min_lower=1, min_upper=1, min_numeric=1, special=false)
# - app_customization_branch: ith
# eventually fill this automatically from calling git using branch name above
# no trailing / leading slash ; relative to app_path ; multiple customizations not yet supported
- app_customization_branch_dir: anpassungen/ith
- max_upload_size: '30M'
- phpversion: '7.2'
- dhparams_file: /etc/ssl/dhparams.pem
tasks:
- name: Check if app_host is set
ansible.builtin.fail:
msg: This playbook requires a FQDN set as app_host
when: app_host is undefined or app_host == '' or app_host == None
- name: Check if app_host resolves to an IPv4 address shared of the target machine
ansible.builtin.debug:
msg: "{{ lookup('community.general.dig', app_host, qtype='A', wantlist=True) | intersect(ansible_facts.all_ipv4_addresses) }}"
- name: Check if app_host resolves to an IPv6 address shared of the target machine
ansible.builtin.debug:
msg: "{{ lookup('community.general.dig', app_host, qtype='AAAA', wantlist=True) | intersect(ansible_facts.all_ipv6_addresses) }}"
All that is left would be to a 3rd task that would fail the playbook when theyre both empty. Currently I am strugging to move the variable from msg:
to when:
cause ansible complains about the use of J2 templating delimiters:
[WARNING]: conditional statements should not include jinja2 templating delimiters such as {{ }} or {% %}. Found: {{ lookup('community.general.dig', app_host, qtype='A', wantlist=True) |
intersect(ansible_facts.all_ipv4_addresses) }}
All I would like to do would be to add both lists of intersections together and ansible.builtin.fail when they union of both intersections I just created is empty. I thought of a when condition like:
{{ {{ lookup('community.general.dig', app_host, qtype='A', wantlist=True) | intersect(ansible_facts.all_ipv4_addresses) }} | union({{ lookup('community.general.dig', app_host, qtype='AAAA', wantlist=True) | intersect(ansible_facts.all_ipv6_addresses) }}) }} == []
but I seem to be not understanding something vital in how to build these expressions.
worked it out: using union()
and intersect()
filters:
{{ lookup('community.general.dig', APP_HOST, qtype='A', wantlist=True) | intersect(ansible_facts.all_ipv4_addresses) }}
This would produce a list of matching ipv4 addresses. Replace ipv4
with ipv6
and set qtype='AAAA'
for ipv6. Then union() both lists.
- name: set up all app services on the target machine
hosts: all
gather_facts: true
gather_subset:
- "!all"
- "!min"
- "network" # in order to get the actual Remote Node configuration
become: yes
vars:
- APP_HOST:
tasks:
- name: Fail when APP_HOST is unset
ansible.builtin.fail:
msg: This playbook requires a FQDN set as APP_HOST
when: APP_HOST is undefined or APP_HOST == '' or APP_HOST == None
- name: Check if IP addresses from node match resolvable IPs for given FQDN
assert:
that:
- lookup('community.general.dig', APP_HOST, qtype='A', wantlist=True) | intersect(ansible_facts.all_ipv4_addresses) != [] or lookup('community.general.dig', APP_HOST, qtype='AAAA', wantlist=True) | intersect(ansible_facts.all_ipv6_addresses) != []
success_msg: "{{ lookup('community.general.dig', APP_HOST, qtype='A', wantlist=True) | intersect(ansible_facts.all_ipv4_addresses) | union(lookup('community.general.dig', APP_HOST, qtype='AAAA', wantlist=True) | intersect(ansible_facts.all_ipv6_addresses)) }}"
fail_msg: "No matching hostname/IP combinations found for {{ APP_HOST }} and {{ ansible_facts.all_ipv4_addresses | union( ansible_facts.all_ipv6_addresses ) }}"
register: certificate_eligible
- name: all done
ansible.builtin.fail:
msg: "{{ certificate_eligible.msg }}"
I couldnt figure out how to populate a variable other than msg
through assert but this is good enough. Output when there is a match:
ansible-playbook -l 'deb11test' -b app_defaults.yaml -e APP_HOST=valid.local.domain
PLAY [set up all app services on the target machine] ******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
TASK [Gathering Facts] **************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
[WARNING]: Platform linux on host deb11test is using the discovered Python interpreter at /usr/bin/python3.9, but future installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.14/reference_appendices/interpreter_discovery.html for more information.
ok: [deb11test]
TASK [Fail when APP_HOST is unset] ************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
skipping: [deb11test]
TASK [Check if IP addresses from node match resolvable IPs for given FQDN] **********************************************************************************************************************************************************************************************************************************************************************************************************************************************
ok: [deb11test] => {
"changed": false,
"msg": [
"192.168.1.134"
]
}
TASK [all done] *********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
fatal: [deb11test]: FAILED! => {"changed": false, "msg": ["192.168.1.134"]}
else when no valid domain is used:
PLAY [set up all app services on the target machine] ******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
TASK [Gathering Facts] **************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
[WARNING]: Platform linux on host deb11test is using the discovered Python interpreter at /usr/bin/python3.9, but future installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.14/reference_appendices/interpreter_discovery.html for more information.
ok: [deb11test]
TASK [Fail when APP_HOST is unset] ************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
skipping: [deb11test]
TASK [Check if IP addresses from node match resolvable IPs for given FQDN] **********************************************************************************************************************************************************************************************************************************************************************************************************************************************
fatal: [deb11test]: FAILED! => {
"assertion": "lookup('community.general.dig', APP_HOST, qtype='A', wantlist=True) | intersect(ansible_facts.all_ipv4_addresses) != [] or lookup('community.general.dig', APP_HOST, qtype='AAAA', wantlist=True) | intersect(ansible_facts.all_ipv6_addresses) != []",
"changed": false,
"evaluated_to": false,
"msg": "No matching hostname/IP combinations found for invalid-domain and ['192.168.1.134', 'fd00::a00:27ff:fe17:2172', 'fe80::a00:27ff:fe17:2172']"
}