Ansible Playbook – How to Handle Existing/Non-Existing Fact in Ansible?

ansibleansible-playbook

For inventory purpose due to some business limitation, i wrote a playbook that retrieve content of resolv.conf + ntp.conf + timesyncd.conf and then, write the retrieved content within a CSV file.

Globally, maybe it's not the best practice but i set a fact at every task for retrieving file content, then i format the datas as i would and finally, write them in a csv like data1;data2;data3;

I got some issues/questions there :

  • if the file doesn't exists,Ansible task fails and doesn't go to the next task, i know this is by design. To prevent that, should it be a good solution to use failed_when/changed_when conditionals near retrieving tasks ?
  • in the last task ("Write results to …") how to handle non-existing fact ? at the moment if 3 facts are existing, file is written. If 1/3 facts doesn't exists but others are , nothing is written.
    Thanks a lot for your advices. Playbook is below
---

- name: sys-check_conf_ntp_dns_net
  hosts: my_servers
  remote_user: my_user

  tasks:
    # RESOLV.CONF
    - name: Retrieve remote /etc/resolv.conf 
      ansible.builtin.slurp:
        src: /etc/resolv.conf
      register: resolv_conf

    - name: Format resolv_conf_fact data
      set_fact:
        resolv_conf_fact: "{{ (resolv_conf['content'] | b64decode) | regex_findall('\\s*nameserver\\s*(.*)') }}"

    # NTP.CONF
    - name: Retrieve remote /etc/ntp.conf 
      ansible.builtin.slurp:
        src: /etc/ntp.conf
      register: ntp_conf

    - name: Format ntp_conf_fact data
      set_fact:
        ntp_conf_fact: "{{ (ntp_conf['content'] | b64decode) | regex_findall('(\\nserver.*?)(\\n)') }}"

    # TIMESYNCD.CONF
    - name: Retrieve /etc/systemd/timesyncd.conf 
      ansible.builtin.slurp:
        src: /etc/systemd/timesyncd.conf
      register: timesyncd_conf

    - name: Format timesyncd_conf_fact data
      set_fact:
        timesyncd_conf_fact: "{{ (timesyncd_conf['content'] | b64decode) | regex_search('(NTP=f.*)') }}"

    - name: Write results to /tmp/sys-check_conf_ntp_dns_net.csv
      lineinfile:
        path: /tmp/sys-check_conf_ntp_dns_net.csv
        line: "Hostname:{{inventory_hostname}};resolv.conf:{{ resolv_conf_fact }};ntp.conf:{{ ntp_conf_fact }};timesyncd.conf:{{ timesyncd_conf_fact }};"
        create: yes
      delegate_to: localhost

EDIT

I finally found a workaround, not sure if it's legal or not 😀
For every fact i set, i add a default value, then as it's filtered through the regex, it behaves like the fact is not empty and so seems to work at the end.

For example :

Setting a fact before :

ntp_conf_fact: "{{ (ntp_conf['content'] | b64decode) | regex_findall('(\\nserver.*?)(\\n)') }}"

Setting a fact after :

ntp_conf_fact: "{{ ((ntp_conf['content']|default([blabla])) | b64decode) | regex_findall('(\\nserver.*?)(\\n)') }}"

Can someone confirm if it sounds ok ? or if anyone got a different solution ?

Best Answer

using default

Your approach with default is absolutely correct and the way to go.

You don't need all the brackets you put, you can write it simply as:

{{ ntp_conf['content'] | default('') | b64decode | regex_findall('(\\nserver.*?)(\\n)') }}

slurp fails if file not exists

Since slurp fails if the file does not exist, the consequence is that the execution is interrupted with failed.

Sample output:

TASK [Retrieve remote /etc/resolv.conf] ************************************************************
ok: [server1]

TASK [Format resolv_conf_fact data] ****************************************************************
ok: [server1]

TASK [Retrieve remote /etc/ntp.conf] ***************************************************************
fatal: [server1]: FAILED! => {"changed": false, "msg": "file not found: /etc/ntp.conf"}

PLAY RECAP *****************************************************************************************
server1     : ok=2    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

To prevent this, you should add an ignore_errors: true to the slurp tasks. This way, if an error occurs, it will be ignored and execution will continue. You handle this exception as described above with the use of default.

Example for the NTP task:

- name: Retrieve remote /etc/ntp.conf
  ansible.builtin.slurp:
    src: /etc/ntp.conf
  ignore_errors: true
  register: ntp_conf

using set_fact or in-task vars

Reading data and storing it in a variable using set_fact works without problems and can be implemented this way.

Note: If you store values in a variable via set_fact, they are available for all following tasks until the end of the runtime (as long as it is not overwritten) and can be used in any number of tasks. The same applies to the data, which were stored by register:.

If you need the data stored by set_fact only once in the task "Write results", you can define the variables directly in the corresponding task without having to formulate separate set_fact tasks.

Note: The variables, which are defined in a task with vars:, are only valid and available within this one task, i.e. they are in the scope of the task.

Your playbook can look like this:

---

- name: sys-check_conf_ntp_dns_net
  hosts: my_servers
  remote_user: my_user

  tasks:
    # RESOLV.CONF
    - name: Retrieve remote /etc/resolv.conf
      ansible.builtin.slurp:
        src: /etc/resolv.conf
      ignore_errors: true
      register: resolv_conf

    # NTP.CONF
    - name: Retrieve remote /etc/ntp.conf
      ansible.builtin.slurp:
        src: /etc/ntp.conf
      ignore_errors: true
      register: ntp_conf

    # TIMESYNCD.CONF
    - name: Retrieve /etc/systemd/timesyncd.conf
      ansible.builtin.slurp:
        src: /etc/systemd/timesyncd.conf
      ignore_errors: true
      register: timesyncd_conf

    - name: Write results to /tmp/sys-check_conf_ntp_dns_net.csv
      lineinfile:
        path: /tmp/sys-check_conf_ntp_dns_net.csv
        line: "Hostname:{{inventory_hostname}};resolv.conf:{{ resolv }};ntp.conf:{{ ntp }};timesyncd.conf:{{ timesyncd }};"
        create: yes
      vars:
        resolv: "{{ resolv_conf['content'] | default('') | b64decode | regex_findall('\\s*nameserver\\s*(.*)') }}"
        ntp: "{{ ntp_conf['content'] | default('') | b64decode | regex_findall('(\\nserver.*?)(\\n)') }}"
        timesyncd: "{{ timesyncd_conf['content'] | default('') | b64decode | regex_search('(NTP=f.*)') }}"
      delegate_to: localhost

If the data is needed only once, the use of vars: has the advantage that the execution of the whole playbook is accelerated, because the number of tasks is reduced.