Score:1

How to handle existing/non existing fact in Ansible?

cn flag

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 :D 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 ?

pt flag
Using the `default` filter is generally how you handle variables that may or may not be defined, so this seems reasonable.
Score:2
pl flag

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.

inframan avatar
cn flag
Thanks phanaz for this answer. Actually i was thinking about setting ignore_errors: true to tasks, but i also found out to avoid to use it too much, i found out to use changed_when:false & failed_when:false. That's a major improvement you made about the vars /set_fact. It's very smart, thanks a lot for this explanation and advices !
phanaz avatar
pl flag
Both variants `ignore_errors: true` and `failed_when: false` work. `ignore_errors` gives you a feedback which files don't exist at all and you can trace it during execution. Likewise, at the end of the execution you will see a count at `ignored`. With `failed_when` all tasks are `ok`, you have no feedback if the files were actually read. I think it is a matter of taste what you want to use.
I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.