Search code examples
ansibleansible-facts

Ansible facts which are guaranteed to be collected for all hosts


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.)


Solution

  • 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