Ansible facts are variables that are automatically discovered by Ansible from a managed host. Facts are pulled by the setup module and contain useful information stored into variables that administrators can reuse. Ansible facts can be part of playbooks, in conditionals, loops, or any other dynamic statement that depends on a value for a managed host
What are facts
- Facts are information discovered by Ansible about a target system
- There are two ways facts are collected:
- Using the setup module with an ad-hoc command:
ansible all -m setup
- Facts are gathered by default when a playbook is executed
- Using the setup module with an ad-hoc command:
- Fact gathering in playbooks may be disabled using the
gather_facts
attribute
How to use facts
- Any collected facts, they may be accessed through variables:
{{ ansible_default_ipv4.address))
- It is possible to use filters with regex, in ad-hoc
mode, to match certain fact names - Facts may also be used with conditionals to have plays behave differently on hosts that meet certain criteria
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[miro@controlnode playbooks]$ ansible managedhost1 -m setup -a "filter=*ipv4" managedhost1 | SUCCESS => { "ansible_facts": { "ansible_default_ipv4": { "address": "172.30.9.51", "alias": "ens192", "broadcast": "172.30.255.255", "gateway": "172.30.8.1", "interface": "ens192", "macaddress": "00:0c:29:75:74:f6", "mtu": 1500, "netmask": "255.255.0.0", "network": "172.30.0.0", "type": "ether" } }, "changed": false } |
The following table shows some of the facts gathered from a managed node that may be useful in a playbook:
Fact | Variable |
Hostname | {{ ansible_hostname }} |
FQDN | {{ ansible_fqdn }} |
Main IPv4 address (based on routing) |
{{ ansible_default_ipv4.address }} |
Main disk first partition size (based on disk name, such as vda, vdb, and so on.) |
{{ ansible_devices.vda.partitions.vda1.size }} |
DNS servers | {{ ansible_dns.nameservers }} |
kernel version | {{ ansible_kernel }} |
When a fact is used in a playbook, Ansible dynamically substitutes the variable name with the corresponding value:
1 2 3 4 5 6 7 8 9 |
[miro@controlnode playbooks]$ cat playbook11.yml --- - hosts: all tasks: - name: Prints various Ansible facts debug: msg: > The default IPv4 address of {{ ansible_fqdn }} is {{ ansible_default_ipv4.address }} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
[miro@controlnode playbooks]$ ansible-playbook playbook11.yml PLAY [all] **************************************************************************************************************************************** TASK [Gathering Facts] **************************************************************************************************************************** ok: [managedhost1] ok: [managedhost1.example.com] ok: [managedhost2] ok: [managedhost2.example.com] TASK [Prints various Ansible facts] *************************************************************************************************************** ok: [managedhost1] => { "msg": "The default IPv4 address of managedhost1.example.com is 172.30.9.51\n" } ok: [managedhost2] => { "msg": "The default IPv4 address of managedhost2.example.com is 172.30.10.25\n" } ok: [managedhost1.example.com] => { "msg": "The default IPv4 address of managedhost1.example.com is 172.30.9.51\n" } ok: [managedhost2.example.com] => { "msg": "The default IPv4 address of managedhost2.example.com is 172.30.10.25\n" } PLAY RECAP **************************************************************************************************************************************** managedhost1 : ok=2 changed=0 unreachable=0 failed=0 managedhost1.example.com : ok=2 changed=0 unreachable=0 failed=0 managedhost2 : ok=2 changed=0 unreachable=0 failed=0 managedhost2.example.com : ok=2 changed=0 unreachable=0 failed=0 |
Administrators can manually disable facts for managed hosts if a large number of servers are being managed. To disable facts, set gather_facts to no in the playbook:
1 2 3 |
--- - hosts: large_farm gather_facts: no |
Facts are always gathered by Ansible unless overridden in this way.
Custom Facts – Facts.d
- It is possible to define custom facts on your servers using the
facts.d
directory - Create /
etc/ansible/facts.d
directory on target system. All valid files within this directory ending in .fact are returned underansible_local
with facts - Fact files may be INI, JSON, or an executable that returns JSON
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
[root@managedhost1 facts.d]# mkdir /etc/ansible/facts.d [root@managedhost1 facts.d]# cd /etc/ansible/facts.d [root@managedhost1 facts.d]# touch data.fact [root@managedhost1 facts.d]# touch prefs.fact [root@managedhost1 facts.d]# touch software_inv.fact [root@managedhost1 facts.d]# su miro [miro@managedhost1 ~]$ tree -A /etc/ansible /etc/ansible tqq ansible.cfg tqq facts.d x tqq data.fact x tqq prefs.fact x mqq software_inv.fact tqq hosts mqq roles [miro@controlnode ansible]$ ansible managedhost1 -m setup -a "filter=ansible_local" managedhost1 | SUCCESS => { "ansible_facts": { "ansible_local": { "data": {}, "prefs": {}, "software_inv": {} } }, "changed": false } |
Listing all facts from the managedhost1
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
[miro@controlnode ansible]$ ansible managedhost1 -m setup | less managedhost1 | SUCCESS => { "ansible_facts": { "ansible_all_ipv4_addresses": [ "172.30.9.51" ], "ansible_all_ipv6_addresses": [ "fe80::6335:25e9:80f:3d5a", "fddb:fe2a:ab1e::c0a8:3", "fe80::bc5f:4f03:654a:9167" ], "ansible_apparmor": { "status": "disabled" }, "ansible_architecture": "x86_64", "ansible_bios_date": "04/05/2016", "ansible_bios_version": "6.00", "ansible_cmdline": { "BOOT_IMAGE": "/vmlinuz-3.10.0-1062.9.1.el7.x86_64", "LANG": "pl_PL.UTF-8", "crashkernel": "auto", "quiet": true, "rd.lvm.lv": "cl/swap", "rhgb": true, "ro": true, "root": "/dev/mapper/cl-root" }, "ansible_date_time": { "date": "2020-02-02", "day": "02", "epoch": "1580680308", "hour": "22", "iso8601": "2020-02-02T21:51:48Z", "iso8601_basic": "20200202T225148534601", "iso8601_basic_short": "20200202T225148", "iso8601_micro": "2020-02-02T21:51:48.534718Z", "minute": "51", "month": "02", "second": "48", "time": "22:51:48", "tz": "CET", "tz_offset": "+0100", "weekday": "niedziela", |
Filter facts from the managedhost1
for example with “ip” word:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
[miro@controlnode ansible]$ ansible managedhost1 -m setup -a "filter=*ip*" managedhost1 | SUCCESS => { "ansible_facts": { "ansible_all_ipv4_addresses": [ "172.30.9.51" ], "ansible_all_ipv6_addresses": [ "fe80::6335:25e9:80f:3d5a", "fddb:fe2a:ab1e::c0a8:3", "fe80::bc5f:4f03:654a:9167" ], "ansible_default_ipv4": { "address": "172.30.9.51", "alias": "ens192", "broadcast": "172.30.255.255", "gateway": "172.30.8.1", "interface": "ens192", "macaddress": "00:0c:29:75:74:f6", "mtu": 1500, "netmask": "255.255.0.0", "network": "172.30.0.0", "type": "ether" }, "ansible_default_ipv6": {}, "ansible_fips": false }, "changed": false } |
Facts about distribution:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[miro@controlnode ansible]$ ansible managedhost1 -m setup -a "filter=*dist*" managedhost1 | SUCCESS => { "ansible_facts": { "ansible_distribution": "CentOS", "ansible_distribution_file_parsed": true, "ansible_distribution_file_path": "/etc/redhat-release", "ansible_distribution_file_variety": "RedHat", "ansible_distribution_major_version": "7", "ansible_distribution_release": "Core", "ansible_distribution_version": "7.7.1908" }, "changed": false } |
Creating local facts on managedhost1
:
1 2 3 4 |
[miro@controlnode ansible]$ ssh managedhost1 Last login: Sun Feb 2 22:55:38 2020 from controlnode.example.com [miro@managedhost1 ~]$ ll /etc/ansible ls: nie ma dostępu do /etc/ansible: Nie ma takiego pliku ani katalogu |
As we se above ansible is not iinstalled on the managedhost1
because it only use ssh on the clients.
1 2 3 4 5 6 7 8 9 10 11 |
[miro@managedhost1 ~]$ sudo mkdir -p /etc/ansible/facts.s [miro@managedhost1 ~]$ sudo mkdir /etc/ansible/facts.d [miro@managedhost1 ~]$ sudo vim /etc/ansible/facts.d [miro@managedhost1 ~]$ sudo vim /etc/ansible/facts.d/prefs.fact [miro@managedhost1 ~]$ cat /etc/ansible/facts.d/prefs.fact [location] type=physical datacenter=Katowice [miro@managedhost1 ~]$ logout Connection to managedhost1 closed. |
On the controlnode we can print local facts from the managedhost1
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[miro@controlnode ansible]$ ansible managedhost1 -m setup -a "filter=ansible_local" managedhost1 | SUCCESS => { "ansible_facts": { "ansible_local": { "prefs": { "location": { "datacenter": "Katowice", "type": "physical" } } } }, "changed": false } |
Example 1.
The Ansible setup module retrieves facts from a system. Run an ad hoc command to retrieve the facts for all servers in the labservers group.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
[miro@controlnode ansible]$ ansible labservers -m setup managedhost1.example.com | SUCCESS => { "ansible_facts": { "ansible_all_ipv4_addresses": [ "172.30.9.51" ], "ansible_all_ipv6_addresses": [ "fe80::6335:25e9:80f:3d5a", "fddb:fe2a:ab1e::c0a8:3", "fe80::bc5f:4f03:654a:9167" ], "ansible_apparmor": { "status": "disabled" }, "ansible_architecture": "x86_64", "ansible_bios_date": "04/05/2016", "ansible_bios_version": "6.00", "ansible_cmdline": { "BOOT_IMAGE": "/vmlinuz-3.10.0-1062.9.1.el7.x86_64", "LANG": "pl_PL.UTF-8", "crashkernel": "auto", "quiet": true, "rd.lvm.lv": "cl/swap", "rhgb": true, "ro": true, "root": "/dev/mapper/cl-root" }, |
Filter the facts matching the ansible_user expression. Append a wildcard to match all facts starting with ansible_user.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
[miro@controlnode ansible]$ ansible labservers -m setup -a 'filter=ansible_user*' managedhost1.example.com | SUCCESS => { "ansible_facts": { "ansible_user_dir": "/home/miro", "ansible_user_gecos": "miro", "ansible_user_gid": 1000, "ansible_user_id": "miro", "ansible_user_shell": "/bin/bash", "ansible_user_uid": 1000, "ansible_userspace_architecture": "x86_64", "ansible_userspace_bits": "64" }, "changed": false } managedhost2.example.com | SUCCESS => { "ansible_facts": { "ansible_user_dir": "/home/miro", "ansible_user_gecos": "miro", "ansible_user_gid": 1000, "ansible_user_id": "miro", "ansible_user_shell": "/bin/bash", "ansible_user_uid": 1000, "ansible_userspace_architecture": "x86_64", "ansible_userspace_bits": "64" }, "changed": false } |
Create a fact file named custom.fact
. The fact file defines the package to install and the service to start on servera. The file should read as follows:
1 2 3 4 |
[general] package = httpd service = httpd state = started |
Create the setup_facts.yml
playbook to make the /etc/ansible/facts.d
remote directory and to save the custom.fact
file to that directory.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[miro@controlnode playbooks]$ cat setup_facts.yml --- - name: Install remote facts hosts: managedhost2 vars: remote_dir: /etc/ansible/facts.d facts_file: custom.fact tasks: - name: Create the remote directory file: state: directory recurse: yes path: "{{ remote_dir }}" - name: Install the new facts copy: src: "{{ facts_file }}" dest: "{{ remote_dir }}" |
Run an ad hoc command with the setup module. Since user-defined facts are put into the ansible_local section, use a filter to display only this section. There should not be any custom facts at this point.
1 2 3 4 5 6 7 |
[miro@controlnode playbooks]$ ansible managedhost2 -m setup -a 'filter=ansible_local' managedhost2 | SUCCESS => { "ansible_facts": { "ansible_local": {} }, "changed": false } |
Run the setup_facts.yml
playbook.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[miro@controlnode playbooks]$ ansible-playbook setup_facts.yml PLAY [Install remote facts] *********************************************************************************************************************** TASK [Gathering Facts] **************************************************************************************************************************** ok: [managedhost2] TASK [Create the remote directory] **************************************************************************************************************** fatal: [managedhost2]: FAILED! => {"changed": false, "msg": "There was an issue creating /etc/ansible as requested: [Errno 13] Brak dostępu: '/etc/ansible'", "path": "/etc/ansible/facts.d", "state": "absent"} to retry, use: --limit @/home/miro/ansible/playbooks/setup_facts.retry PLAY RECAP **************************************************************************************************************************************** managedhost2 : ok=1 changed=0 unreachable=0 failed=1 |
Run with become a root parameter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[miro@controlnode playbooks]$ ansible-playbook setup_facts.yml -b PLAY [Install remote facts] *********************************************************************************************************************** TASK [Gathering Facts] **************************************************************************************************************************** ok: [managedhost2] TASK [Create the remote directory] **************************************************************************************************************** changed: [managedhost2] TASK [Install the new facts] ********************************************************************************************************************** changed: [managedhost2] PLAY RECAP **************************************************************************************************************************************** managedhost2 : ok=3 changed=2 unreachable=0 failed=0 |
To ensure the new facts have been properly installed, run an ad hoc command with the setup module again. Display only the ansible_local section. The custom facts should appear.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[miro@controlnode playbooks]$ ansible managedhost2 -m setup -a 'filter=ansible_local' managedhost2 | SUCCESS => { "ansible_facts": { "ansible_local": { "custom": { "general": { "package": "httpd", "service": "httpd", "state": "started" } } } }, "changed": false } |
It is now possible to create the main playbook that uses both default and user facts to configure managedhost2. The first task installs the httpd package. Use the user fact for the name of the package. Second task uses the custom fact to start the httpd service.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[miro@controlnode playbooks]$ cat playbook12.yml --- - name: Install Apache and starts the service become: yes hosts: managedhost2 tasks: - name: Install the required package yum: name: "{{ ansible_local.custom.general.package }}" state: latest - name: Start the service service: name: "{{ ansible_local.custom.general.service }}" state: "{{ ansible_local.custom.general.state }}" |
Before running the playbook, use an ad hoc command to verify the httpd service is not currently running on managedhost2:
1 2 3 4 5 6 7 8 9 10 11 |
[miro@controlnode playbooks]$ ansible managedhost2 -m command -a 'systemctl status httpd' managedhost2 | FAILED | rc=3 >> ● httpd.service - The Apache HTTP Server Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled) Active: inactive (dead) since wto 2020-01-28 19:08:32 CET; 2 months 29 days ago Docs: man:httpd(8) man:apachectl(8) Main PID: 4432 (code=exited, status=0/SUCCESS) Status: "Total requests: 0; Current requests/sec: 0; Current traffic: 0 B/sec" Warning: Journal has been rotated since unit was started. Log output is incomplete or unavailable.non-zero return code |
Run the playbook using the ansible-playbook command. Watch the output as Ansible starts by installing the package, then enabling the service.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[miro@controlnode playbooks]$ ansible-playbook playbook12.yml PLAY [Install Apache and starts the service] ****************************************************************************************************** TASK [Gathering Facts] **************************************************************************************************************************** ok: [managedhost2] TASK [Install the required package] *************************************************************************************************************** changed: [managedhost2] TASK [Start the service] ************************************************************************************************************************** changed: [managedhost2] PLAY RECAP **************************************************************************************************************************************** managedhost2 : ok=3 changed=2 unreachable=0 failed=0 |
Use an ad hoc command to execute systemctl to check if the httpd service is now running on managedhost2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
[miro@controlnode playbooks]$ ansible -b managedhost2 -m command -a 'systemctl status httpd' managedhost2 | SUCCESS | rc=0 >> ● httpd.service - The Apache HTTP Server Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled) Active: active (running) since wto 2020-04-28 14:30:54 CEST; 1min 6s ago Docs: man:httpd(8) man:apachectl(8) Main PID: 4804 (httpd) Status: "Total requests: 0; Current requests/sec: 0; Current traffic: 0 B/sec" CGroup: /system.slice/httpd.service ├─4804 /usr/sbin/httpd -DFOREGROUND ├─4805 /usr/sbin/httpd -DFOREGROUND ├─4806 /usr/sbin/httpd -DFOREGROUND ├─4807 /usr/sbin/httpd -DFOREGROUND ├─4808 /usr/sbin/httpd -DFOREGROUND └─4809 /usr/sbin/httpd -DFOREGROUND kwi 28 14:30:53 managedhost2.example.com systemd[1]: Starting The Apache HTTP Server... kwi 28 14:30:54 managedhost2.example.com systemd[1]: Started The Apache HTTP Server. |