3

If I run a playbook from start to finish, it gathers facts and then runs roles. But usually I don't do that: I just run roles directly (via their tags). So the facts are not gathered, and I get errors. To fix this I must remember to run a special "setup" task:

$ ansible-playbook playbook.yml -t setup,my-role

I often forget to do that, get errors and waste time. So I want each role to start with a fail-safe task that automatically gathers facts if necessary:

- setup:
  when: ansible_os_family is undefined

That works. But in other questions I've read that not all facts are collected across all hosts - apparently there are differences.

I chose ansible_os_family but I'm worried that it's not "universal".

Are there any facts that are 100% guaranteed to be collected across all hosts? (I don't need an exhaustive list, just a few, or even one, for this use case.)

lonix
  • 14,255
  • 23
  • 85
  • 176
  • 3
    `hostvars[inventory_hostname]` is an antipattern; `hostvars` is for accessing the variables of *other* hosts, not the current one. All you need there is `when: ansible_os_family is undefined`. – flowerysong Jun 20 '23 at 03:22
  • @flowerysong Thank you for that little bit of wisdom - I have many find-and-replace to make in my playbook now! :-) – lonix Jun 20 '23 at 03:24
  • 2
    Personally, I tend to use the fact(s) that I'm going to be using in the role in the conditional, since that's actually what I care about being defined, and it's possible for partial fact gathering to happen (e.g. `gather_subset` and `filter`) so no particular fact guarantees anything else is present. See e.g. https://github.com/its-core-applications/email-ansible-aws/blob/a5effeec050df24fa58482d03b87026e673b6070/ansible/roles/build/tasks/main.yml#L4-L5 – flowerysong Jun 20 '23 at 03:37
  • @flowerysong In that line you linked to, you use `- gather_facts:` instead of `- setup:` is there a difference? – lonix Jun 20 '23 at 04:14
  • 2
    `gather_facts` runs all of the [configured](https://docs.ansible.com/ansible/latest/reference_appendices/config.html#facts-modules) facts modules, like fact gathering at the play level. `setup` runs the setup module. (Currently unless you're managing network devices there's no practical difference with the default config, but there are vague plans to replace the monolithic `setup` module with smaller modules that can be run in parallel.) – flowerysong Jun 20 '23 at 04:24
  • @flowerysong If I'm understanding you correctly, to be forward-compatible, I should use `gather_facts:` instead. – lonix Jun 20 '23 at 04:38

3 Answers3

5

I think a more robust solution is to use the always tag on a play that gather facts. Consider this playbook:

- hosts: all
  gather_facts: true
  tags: [always]

- hosts: all
  gather_facts: false
  tasks:
    - tags: [example]
      debug:
        msg: this is another play

Even if I run ansible-playbook playbook.yaml -t example, it will still run the first play (which runs the implicit setup task):

$ ansible-playbook -i hosts.yaml playbook.yaml  -t example

PLAY [all] **********************************************************************************************

TASK [Gathering Facts] **********************************************************************************
ok: [node2]
ok: [node1]

PLAY [all] **********************************************************************************************

TASK [debug] ********************************************************************************************
ok: [node1] => {
    "msg": "this is another play"
}
ok: [node2] => {
    "msg": "this is another play"
}

PLAY RECAP **********************************************************************************************
node1                      : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
node2                      : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

You can of course use the always tag on a specific task instead; that is, you could rewrite your example as:

- setup:
  tags: [always]

But doing it at the play level lets you take advantage of Ansible's smart gathering and fact caching capabilities.

larsks
  • 277,717
  • 41
  • 399
  • 399
  • Thanks larsks. This would work for most people for most cases. I didn't do it that way as my first role provisions the bare bones server, changes ssh ports, root passwords, etc. Once that's done it causes conflicts with the rest of the play - so I don't gather facts, rather, I run that stuff, then run `setup:`, then other roles. That works. But for latter roles, I'd like a fail-safe setup task as mentioned above (in case I run them via tags). Do you happen to know whether there are any facts that are gathered for all OSs / environments / etc.? – lonix Jun 20 '23 at 03:14
  • 2
    If your use of `anisble_os_family` is working, that seems to be reasonable. – larsks Jun 20 '23 at 03:20
3

Q: "I think ansible_os_family is a reasonable choice ... If you have a definitive reference, please add it as an answer."


Variable ansible_local

A: Create such a 'definitive' reference on your own. The setup module provides the parameter fact_path for this purpose. For example, test it on the localhost first. Create JSON file

shell> cat /etc/ansible/facts.d/misc.fact 
{"run_setup": false}

The playbook

shell> cat pb.yml
- hosts: localhost
  gather_facts: true
  tasks:
    - debug:
        var: ansible_local

gives (abridged)

  ansible_local:
    misc:
      run_setup: false

In your use case, you'll have to copy the file misc.fact to the remote hosts. Create a project for testing

shell> tree .
.
├── ansible.cfg
├── hosts
├── misc.fact
└── pb.yml
shell> cat ansible.cfg
[defaults]
gathering = explicit
collections_path = $HOME/.local/lib/python3.9/site-packages/
inventory = $PWD/hosts
roles_path = $PWD/roles
remote_tmp = ~/.ansible/tmp
retry_files_enabled = false
stdout_callback = yaml
shell> cat hosts 
test_11
test_13
shell> cat misc.fact 
{"run_setup": false}

Test it in a single play here to demonstrate the idea. In your use case, keep the block to manage the files either in the playbook or put it into the roles. Put the conditional setup into the roles.

shell> cat pb.yml 
- hosts: all
  gather_facts: false

  pre_tasks:

    - name: Manage ansible_local.misc facts
      block:
        - file:
            state: directory
            path: /etc/ansible/facts.d
        - copy:
            src: misc.fact
            dest: /etc/ansible/facts.d/misc.fact
            mode: 0644
      become: true

    - setup:
      when: ansible_local.misc.run_setup|d(true)

  tasks:

    - debug:
        var: ansible_local.misc.run_setup

gives

shell> ansible-playbook pb.yml

PLAY [all] ************************************************************************************

TASK [file] ***********************************************************************************
changed: [test_11]
changed: [test_13]

TASK [copy] ***********************************************************************************
changed: [test_13]
changed: [test_11]

TASK [setup] **********************************************************************************
ok: [test_13]
ok: [test_11]

TASK [debug] **********************************************************************************
ok: [test_11] => 
  ansible_local.misc.run_setup: false
ok: [test_13] => 
  ansible_local.misc.run_setup: false

PLAY RECAP ************************************************************************************
test_11: ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
test_13: ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Cache facts

A: I still believe a better solution is to cache facts. Set DEFAULT_GATHERING smart

smart: each new host that has no facts discovered will be scanned, but if the same host is addressed in multiple plays it will not be contacted again in the run.

and enable fact cache plugin. For example,

shell> cat ansible.cfg
[defaults]
gathering = smart
#
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_cache.json
fact_caching_prefix = ansible_facts_
fact_caching_timeout = 86400
#
collections_path = $HOME/.local/lib/python3.9/site-packages/
inventory = $PWD/hosts
roles_path = $PWD/roles
remote_tmp = ~/.ansible/tmp
retry_files_enabled = false
stdout_callback = yaml

Given the project for testing

shell> tree .
.
├── ansible.cfg
├── hosts
├── pb.yml
└── roles
    └── roleA
        └── tasks
            └── main.yml
shell> cat hosts
test_11
test_13
shell> cat roles/roleA/tasks/main.yml 
- debug:
    var: ansible_os_family
shell> cat pb.yml 
- hosts: all
  roles:
    - roleA

The facts are gathered for the first time

shell> ansible-playbook pb.yml 

PLAY [all] ************************************************************************************

TASK [Gathering Facts] ************************************************************************
ok: [test_11]
ok: [test_13]

TASK [roleA : debug] **************************************************************************
ok: [test_11] => 
  ansible_os_family: FreeBSD
ok: [test_13] => 
  ansible_os_family: FreeBSD

PLAY RECAP ************************************************************************************
test_11: ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
test_13: ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

and cached

shell> tree /tmp/ansible_cache.json/
/tmp/ansible_cache.json/
├── ansible_facts_test_11
└── ansible_facts_test_13

Next time you run a playbook the cache is used

shell> ansible-playbook pb.yml 

PLAY [all] ************************************************************************************

TASK [roleA : debug] **************************************************************************
ok: [test_11] => 
  ansible_os_family: FreeBSD
ok: [test_13] => 
  ansible_os_family: FreeBSD

PLAY RECAP ************************************************************************************
test_11: ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
test_13: ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Notes:

  • You say you don't gather facts. You instead run the module setup after provisioning and then run other roles. This is the best use case for the described framework.

  • See other cache plugins

shell> ansible-doc -t cache -l
  • Set CACHE_PLUGIN_TIMEOUT to your needs. The gathered facts will be updated after a timeout or on demand. Please test it
shell> cat roles/roleB/tasks/main.yml
- debug:
    var: ansible_date_time.iso8601_micro
shell> cat roles/roleC/tasks/main.yml
- setup:
    gather_subset: date_time
- debug:
    var: ansible_date_time.iso8601_micro
shell> cat pb.yml
- hosts: all
  roles:
    - roleB
    - roleC

a) Running repeatedly roleB and roleC. The first role uses the cached fact ansible_date_time. The last role updates the cache

shell> ansible-playbook -l test_11 pb.yml 

PLAY [all] ************************************************************************************

TASK [roleB : debug] **************************************************************************
ok: [test_11] => 
  ansible_date_time.iso8601_micro: '2023-06-20T08:10:56.219945Z'

TASK [roleC : setup] **************************************************************************
ok: [test_11]

TASK [roleC : debug] **************************************************************************
ok: [test_11] => 
  ansible_date_time.iso8601_micro: '2023-06-20T08:11:15.289719Z'

PLAY RECAP ************************************************************************************
test_11: ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
shell> ansible-playbook -l test_11 pb.yml 

PLAY [all] ************************************************************************************

TASK [roleB : debug] **************************************************************************
ok: [test_11] => 
  ansible_date_time.iso8601_micro: '2023-06-20T08:11:15.289719Z'

TASK [roleC : setup] **************************************************************************
ok: [test_11]

TASK [roleC : debug] **************************************************************************
ok: [test_11] => 
  ansible_date_time.iso8601_micro: '2023-06-20T08:11:39.579222Z'

PLAY RECAP ************************************************************************************
test_11: ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

b) Set ANSIBLE_CACHE_PLUGIN_TIMEOUT=-1 if you want to update the cache

shell> ANSIBLE_CACHE_PLUGIN_TIMEOUT=-1 ansible-playbook -l test_11 pb.yml 

PLAY [all] ************************************************************************************

TASK [roleB : debug] **************************************************************************
ok: [test_11] => 
  ansible_date_time.iso8601_micro: '2023-06-20T08:28:40.088411Z'

TASK [roleC : setup] **************************************************************************
ok: [test_11]

TASK [roleC : debug] **************************************************************************
ok: [test_11] => 
  ansible_date_time.iso8601_micro: '2023-06-20T08:28:43.891752Z'

PLAY RECAP ************************************************************************************
test_11: ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
Vladimir Botka
  • 58,131
  • 4
  • 32
  • 63
0

The other answers are excellent, and I recommend you upvote them instead.

However, they provide different ways to solve this problem, and I still want a solution to the problem as stated. So for completion's sake, here's my solution.

playbook.yml:

---
- hosts: all
  gather_facts: false
  pre_tasks:
    - setup:
      tags: [ never, setup ]
  roles:
    - role: provision           # runs setup module
      tags: provision
    - role: traefik
      tags: traefik
    - role: portainer
      tags: portainer
  • The playbook is is run on a brand new server
  • The provision role makes major changes (e.g. configures root password, ssh settings), so would thereafter cause the playbook to fail. That's why I have gather_facts: false above. Once the provision role finishes making major changes, it runs setup: to gather facts. Everything after that proceeds as normal.
  • If the playbook is run completely, then the following roles (traefik and portainer) will have access to facts.
  • But if I ever run any roles independently, e.g. $ ansible-playbook playbook.yml -t traefik, then facts would not have been gathered, and I'll get errors.

The simple solution is the setup task above. It is used like this: $ ansible-playbook playbook.yml -t setup,traefik. But I often forget to include it, and get errors.

A better solution is what I explained in the question. At the beginning of each role I have this:

e.g. roles/traefik/tasks/main.yml:

---
- gather_facts:
  when: ansible_os_family is undefined
# ...etc...

If the ansible_os_family fact is unset, that task will run and gather facts, and the role would succeed. Very simple and effective.

The only worry is one should choose a fact guaranteed to exist for all OSs and environments. Many SO questions show that not all facts are available for all hosts, so it's important to choose well.

I think ansible_os_family is a reasonable choice. However, I don't know for sure, that's why I opened this question. If you have a definitive reference, please add it as an answer.

lonix
  • 14,255
  • 23
  • 85
  • 176