Ansible

Ansible OS Family and Distribution Facts: Full List (2026)

On every Red Hat family system we gathered facts from (RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream and Fedora), ansible_facts['os_family'] comes back as RedHat. Debian and Ubuntu both return Debian, while Arch and Alpine return their own names. The fact that tells them apart is ansible_facts['distribution'], and it’s often not the string you’d guess: RHEL reports RedHat, Oracle Linux reports OracleLinux, and CentOS Stream still reports CentOS.

Original content from computingforgeeks.com - post 74988

The tables below list the Ansible os_family, distribution, version, release, package manager and service manager facts exactly as the setup module returned them on twelve systems. After that you get the family map ansible-core uses for the systems we couldn’t boot, the errors we hit, and a tested playbook that branches on these values. Every fact here was gathered with ansible-core 2.21.5 from an Ubuntu 26.04 controller in October 2026.

Check Ansible os_family values by OS

Match on os_family when a task only cares about the package and service tooling, and on distribution plus distribution_major_version when the release matters. Every value is a string, including the version numbers, so compare them in quotes. This is what the setup module returned on each lab host:

Tested OSos_familydistributiondistribution_major_versiondistribution_versiondistribution_release
Rocky Linux 10RedHatRocky1010.1Red Quartz
AlmaLinux 9RedHatAlmaLinux99.4Seafoam Ocelot
RHEL 10RedHatRedHat1010.1Coughlan
CentOS Stream 10RedHatCentOS1010Stream
Oracle Linux 10RedHatOracleLinux1010.1NA
Oracle Linux 9RedHatOracleLinux99.8NA
Fedora 44RedHatFedora4444(empty string)
Ubuntu 26.04DebianUbuntu2626.04resolute
Ubuntu 24.04DebianUbuntu2424.04noble
Debian 13DebianDebian1313.4trixie
Arch LinuxArchlinuxArchlinuxNANANA
Alpine Linux 3.24AlpineAlpine33.24.2NA

Twelve systems collapse into only four family values, which is the whole point of os_family: seven of them share RedHat, so you can cover RHEL, its rebuilds, CentOS Stream and Fedora with one when: line. The release column is the one you can’t lean on. Rocky, AlmaLinux and RHEL fill it with a code name, Oracle Linux and Alpine return the literal string NA, and Fedora returns nothing at all. The only place CentOS Stream gives itself away is that release column, where it says Stream, so a play that must skip Stream has to test it there.

The second set of facts decides how a task installs packages and manages services, and it splits the RedHat family in ways the table above hides:

Tested OSpkg_mgrservice_mgrdistribution_file_varietydistribution_file_pathPython used
Rocky Linux 10dnfsystemdRedHat/etc/redhat-release3.12.11
AlmaLinux 9dnfsystemdRedHat/etc/redhat-release3.9.18
RHEL 10dnfsystemdRedHat/etc/redhat-release3.12.11
CentOS Stream 10dnfsystemdCentOS/etc/centos-release3.12.15
Oracle Linux 10dnfsystemdOracleLinux/etc/oracle-release3.12.12
Oracle Linux 9dnfsystemdOracleLinux/etc/oracle-release3.9.25
Fedora 44dnf5systemdRedHat/etc/redhat-release3.14.3
Ubuntu 26.04aptsystemdDebian/etc/os-release3.14.4
Ubuntu 24.04aptsystemdDebian/etc/os-release3.12.3
Debian 13aptsystemdDebian/etc/os-release3.13.5
Arch LinuxpacmansystemdArchlinux/etc/arch-release3.14.8
Alpine Linux 3.24apkopenrcAlpine/etc/alpine-release3.14.8

Fedora is the odd one out on pkg_mgr: it reports dnf5, while every EL 9 and EL 10 system reports dnf, so a condition written for one misses the other. distribution_file_variety is a different trap. It names the release file Ansible parsed, not the distribution, which is why AlmaLinux, Rocky and Fedora all show RedHat there. Alpine is the only tested system without systemd, and its openrc value matters if your roles call ansible.builtin.systemd directly.

The Python column is good news if you still run EL 9: ansible-core 2.21 managed the Python 3.9 that ships there, so you do not need a newer interpreter on those hosts just to gather facts.

Run the setup module to gather facts

You do not need a playbook to see these values. The ansible.builtin.setup module runs as an ad-hoc command, and its filter argument takes a shell-style pattern, so you can print only the distribution facts instead of several hundred lines of hardware data. If Ansible isn’t on the controller yet, follow the Ansible install on Ubuntu 26.04 first (our controller ran ansible-core from pipx). Start with the controller itself over a local connection:

ansible localhost -c local -m ansible.builtin.setup -a 'filter=ansible_distribution*'

Expect a warning about the inventory when you run against implicit localhost. Below it, the seven distribution facts print with their full ansible_ names:

[WARNING]: No inventory was parsed, only implicit localhost is available
localhost | SUCCESS => {
    "ansible_facts": {
        "ansible_distribution": "Ubuntu",
        "ansible_distribution_file_parsed": true,
        "ansible_distribution_file_path": "/etc/os-release",
        "ansible_distribution_file_variety": "Debian",
        "ansible_distribution_major_version": "26",
        "ansible_distribution_release": "resolute",
        "ansible_distribution_version": "26.04"
    },
    "changed": false
}

You can also pass a comma-separated list to the filter, which is the quickest way to check the facts your role branches on. This pulls the family, package manager, service manager, Python and kernel in one call:

ansible localhost -c local -m ansible.builtin.setup -a 'filter=ansible_os_family,ansible_pkg_mgr,ansible_service_mgr,ansible_python_version,ansible_kernel'

On the Ubuntu 26.04 controller it returned these values:

[WARNING]: No inventory was parsed, only implicit localhost is available
localhost | SUCCESS => {
    "ansible_facts": {
        "ansible_kernel": "7.0.0-27-generic",
        "ansible_os_family": "Debian",
        "ansible_pkg_mgr": "apt",
        "ansible_python_version": "3.14.4",
        "ansible_service_mgr": "systemd"
    },
    "changed": false
}

For a remote host that is not in any inventory yet, pass its address with a trailing comma, which tells Ansible the value is a host list and not an inventory file. Change the address and the -u user to your own host and login. We rebuilt the same target for each OS, so host key checking was switched off for the lab. Leave it on for real servers:

ansible all -i '10.0.1.254,' -u josphat --ssh-common-args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' -m ansible.builtin.setup

Each run opened with an interpreter discovery warning and then the full fact set. This one is from Rocky Linux 10, and the JSON body is trimmed here:

[WARNING]: Host '10.0.1.254' is using the discovered Python interpreter at '/usr/bin/python3.12', but future installation of another Python interpreter could cause a different interpreter to be discovered. See https://docs.ansible.com/ansible-core/2.21/reference_appendices/interpreter_discovery.html for more information.
10.0.1.254 | SUCCESS => {

In playbooks, read every value through ansible_facts['os_family'] and stop using the top-level ansible_os_family. The inject_facts_as_vars setting still defaults to true in the ansible-core 2.21 config definitions, so the old names still resolve. That default has been deprecated since ansible-core 2.20. The 2.20 changelog says it switches to false in 2.24. Once it does, every ansible_os_family in your roles becomes undefined without any config change on your side. The current release already prints a deprecation warning each time a playbook reads an injected fact. The troubleshooting section below shows the exact message.

Inside ansible_facts the ansible_ prefix is dropped, so ansible_distribution becomes ansible_facts['distribution']. The Ansible variables guide covers how facts sit next to inventory and play variables. Every playbook on this page uses the dictionary form, which is why none of their logged runs show that warning.

Read the values that break when conditions

Most broken OS conditions come from a handful of values that look wrong until you’ve seen them on a real host. These are the ones the lab returned that a hand-written condition usually gets wrong:

  • Fedora 44: pkg_mgr is dnf5, not dnf
  • RHEL 10: distribution is RedHat, never RHEL
  • CentOS Stream 10: distribution is CentOS and distribution_version is 10 with no minor
  • Arch Linux: version, major version and release are all NA
  • Alpine 3.24: distribution_major_version is 3
  • Oracle Linux 9 and 10: distribution_release is NA

The Arch and Alpine rows come from the same code path in ansible-core’s distribution.py. When the parser falls back to os-release, any version field that file doesn’t provide is set to NA, and the major version is whatever comes before the first dot of distribution_version. Some release-file parsers (Debian, SUSE, Amazon, Flatcar) set the major version themselves, so this rule is specific to the os-release fallback.

The split rule is why Alpine’s major version is just 3, so a condition on distribution_major_version == "3" matches every Alpine 3.x release, and a rolling distro like Arch has nothing to compare at all.

If you cast versions with the int filter for a greater-than test, remember that it turns NA into 0 instead of failing (our test printed NA as int = 0 on ansible-core 2.21.5). Your check then passes or fails for the wrong reason, and an os_family guard won’t save you, because Arch is its own family and still reports NA. Guard on the value itself with ansible_facts['distribution_major_version'] != 'NA', or compare real versions with the version test, as in ansible_facts['distribution_version'] is version('10', '>='), instead of | int. In our run that test returned true for 26.04 and 10.1 and false for 9.8, and on NA it failed the task with Version comparison failed: '<' not supported between instances of 'str' and 'int', which is the loud failure you want instead of a silent 0.

For the dnf5 split, the cleaner fix is not to test pkg_mgr at all. ansible.builtin.package picks the right backend from that fact on its own. You only need pkg_mgr in a condition when you call ansible.builtin.dnf or ansible.builtin.dnf5 by name. If you check whether a package is present before acting on it, the package_facts approach works the same on dnf, dnf5 and apt hosts.

Find os_family for untested distributions

For systems you don’t have in a lab, the answer lives in one dictionary. ansible-core sets os_family from OS_FAMILY_MAP in distribution.py, and if a distribution isn’t listed there, os_family falls back to the distribution name itself. These are the mappings in the 2.21.5 source, copied as written:

os_familydistribution values that map to it
RedHatRedHat, RHEL, Fedora, CentOS, Scientific, SLC, Ascendos, CloudLinux, PSBM, OracleLinux, OVS, OEL, Amazon, Amzn, Virtuozzo, XenServer, Alibaba, EulerOS, openEuler, AlmaLinux, Rocky, TencentOS, EuroLinux, Kylin Linux Advanced Server, MIRACLE
DebianDebian, Ubuntu, Raspbian, Neon, KDE neon, Linux Mint, SteamOS, Devuan, Kali, Cumulus Linux, Pop!_OS, Parrot, Pardus GNU/Linux, Uos, Deepin, OSMC, Linux Mint Debian Edition, Univention Corporate Server
SuseSuSE, SLES, SLED, openSUSE, openSUSE Tumbleweed, SLES_SAP, SUSE_LINUX, openSUSE Leap, ALP-Dolomite, SL-Micro, openSUSE MicroOS
ArchlinuxArchlinux, Antergos, Manjaro
GentooGentoo, Funtoo
AlpineAlpine
SolarisSolaris, Nexenta, OmniOS, OpenIndiana, SmartOS
FreeBSDFreeBSD, TrueOS
DarwinMacOSX
AIXAIX
HP-UXHPUX
MandrakeMandrake, Mandriva
SlackwareSlackware
AltlinuxAltlinux
SMGLSMGL
ClearLinuxClear Linux OS, Clear Linux Mix
NetBSDNetBSD
DragonFlyDragonflyBSD, DragonFlyBSD, Gentoo/DragonflyBSD, Gentoo/DragonFlyBSD

Read this table as the source’s intent, not as tested output. We tried to add openSUSE Leap to the lab, but both rebuilds refused every SSH key. The Suse row therefore comes from the source, not from a host. Kali, FreeBSD, SLES and the commercial Unix rows weren’t booted either. If you run openSUSE, start with the Ansible on openSUSE Leap guide and confirm the Suse values with the filter command above.

Watch the fallback rule, too. The parser recognises some distributions that the map does not list, and each of them gets an os_family equal to its own name:

  • OpenWrt, read from /etc/openwrt_release
  • VMwareESX, read from /etc/vmware-release
  • Coreos, read from /etc/coreos/update.conf
  • Flatcar, read from /etc/os-release

A play that only branches on RedHat and Debian silently skips all four, so give them a default vars file or an explicit distribution condition if you manage any of them.

Windows does not go through this map at all. Windows facts come from the PowerShell implementation of the setup module, and we didn’t run one for this page, so check a real host before you write a Windows condition. The Ansible for Windows Server tutorial covers the WinRM side.

Troubleshooting fact gathering errors

Four messages came up while we collected the tables above and tested the playbooks, and you will probably see them too. Only the first one stops the run, but the others show up on every host or every play and are worth understanding before they end up in a CI log.

ansible: error: argument –ssh-common-args: expected one argument

Our first run passed the SSH options as a separate word, --ssh-common-args '-o StrictHostKeyChecking=no ...', and ansible-core’s argument parser rejected it because the value starts with -o and looks like another flag. The command printed its usage text and exited with code 2. Attach the value with an equals sign, as in --ssh-common-args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null', and it parses.

[DEPRECATION WARNING]: The ‘-t’ argument is deprecated

Older guides save facts per host with ansible all -m setup -t <dir>. It still works on 2.21, but each run prints two deprecation warnings, and both say the feature goes away in ansible-core 2.23. Redirect the ad-hoc output to a file instead, or use ad-hoc commands with a filter, as above, so nothing depends on -t.

[WARNING]: Host is using the discovered Python interpreter

Every remote run printed this warning, and every run still succeeded. Discovery picked /usr/bin/python3.9 on AlmaLinux 9 and Oracle Linux 9, python3.12 on EL 10 and Ubuntu 24.04, python3.13 on Debian 13, and python3.14 on Fedora, Arch and Alpine. The warning only says that a second Python installed later could change the choice. If your hosts carry several interpreters, set ansible_python_interpreter per group in the inventory, which the inventory management guide shows, and the warning has nothing left to warn about.

[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.

This warning appears the moment a playbook reads a fact by its top-level name. To see it, we saved a two-task playbook as /tmp/dep.yml on the Ubuntu 26.04 controller and ran it against localhost with no ansible.cfg changes. The first task reads ansible_os_family, the second reads the same fact from ansible_facts:

- hosts: localhost
  gather_facts: true
  tasks:
    - ansible.builtin.debug:
        msg: "{{ ansible_os_family }}"
    - ansible.builtin.debug:
        msg: "{{ ansible_facts['os_family'] }}"

Only the first task triggers the warning, and the Origin lines point at the exact file, line and column that used the injected name. Both tasks still print Debian, because the injection default is still true on 2.21:

[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [localhost] ***************************************************************
TASK [Gathering Facts] *********************************************************
ok: [localhost]
TASK [ansible.builtin.debug] ***************************************************
[WARNING]: Deprecation warnings can be disabled by setting `deprecation_warnings=False` in ansible.cfg.
[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24.
Origin: /tmp/dep.yml:5:14
3   tasks:
4     - ansible.builtin.debug:
5         msg: "{{ ansible_os_family }}"
               ^ column 14
Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead.
ok: [localhost] => {
    "msg": "Debian"
}
TASK [ansible.builtin.debug] ***************************************************
ok: [localhost] => {
    "msg": "Debian"
}
PLAY RECAP *********************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Don’t silence it with deprecation_warnings=False, because that setting hides every future deprecation too. Search your roles for top-level names such as ansible_os_family, ansible_distribution and ansible_pkg_mgr, and rewrite each one as ansible_facts['os_family'] and so on, without the prefix. When the warning stops printing, your playbooks will keep working after the 2.24 default change.

Use os_family and distribution in playbooks

The pattern that holds up on a large inventory keeps the per-OS differences in vars files and loads the most specific file that exists. Use when: only for tasks that truly belong to one family or one release. The playbook below does both, and it adds a group_by step so later plays can target a whole family by group name. It only prints what it would do, which keeps it safe to run against any host. We ran it with an Oracle Linux 10 target and the Ubuntu 26.04 controller, then again with Arch Linux as the target. Create the project directory first:

mkdir -p ~/osfacts-demo/vars
cd ~/osfacts-demo

Your inventory needs one remote host and the controller itself. Change ansible_host and ansible_user to a machine you can reach over SSH:

vim ~/osfacts-demo/inventory.ini

Add the two hosts and the lab SSH options. The [lab:vars] line turns off host key checking because our node01 was rebuilt for every OS, so delete it for servers whose keys don’t change:

[lab]
node01 ansible_host=10.0.1.254 ansible_user=josphat
dev01 ansible_connection=local

[lab:vars]
ansible_ssh_common_args='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'

Next, the playbook. The include_vars task uses the first_found lookup and registers its result, so the next task can print which file won:

vim ~/osfacts-demo/os-branching.yml

Paste the full playbook:

---
- name: Branch on OS facts
  hosts: lab
  gather_facts: true
  tasks:
    - name: Show the facts this playbook branches on
      ansible.builtin.debug:
        msg: >-
          {{ inventory_hostname }}: os_family={{ ansible_facts['os_family'] }}
          distribution={{ ansible_facts['distribution'] }}
          major={{ ansible_facts['distribution_major_version'] }}
          file_variety={{ ansible_facts['distribution_file_variety'] | default('n/a') }}
          pkg_mgr={{ ansible_facts['pkg_mgr'] }}

    - name: Load the most specific vars file that exists
      ansible.builtin.include_vars:
        file: "{{ lookup('ansible.builtin.first_found', params) }}"
      vars:
        params:
          files:
            - "{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_major_version'] }}.yml"
            - "{{ ansible_facts['distribution'] }}.yml"
            - "{{ ansible_facts['os_family'] }}.yml"
            - default.yml
          paths:
            - vars
      register: os_vars

    - name: Show which vars file won and the package name it sets
      ansible.builtin.debug:
        msg: "{{ os_vars.ansible_included_var_files[0] | basename }} -> install {{ web_package }}, manage {{ web_service }}"

    - name: Runs only on the RedHat family
      ansible.builtin.debug:
        msg: "RedHat family host, package manager {{ ansible_facts['pkg_mgr'] }}"
      when: ansible_facts['os_family'] == "RedHat"

    - name: Runs only on Debian family
      ansible.builtin.debug:
        msg: "Debian family host: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}"
      when: ansible_facts['os_family'] == "Debian"

    - name: Runs only on Oracle Linux 10
      ansible.builtin.debug:
        msg: "Oracle Linux 10, EPEL comes from {{ epel_release_package | default('n/a') }}"
      when:
        - ansible_facts['distribution'] == "OracleLinux"
        - ansible_facts['distribution_major_version'] == "10"

    - name: Group hosts by OS family
      ansible.builtin.group_by:
        key: "os_{{ ansible_facts['os_family'] }}"

- name: Play that targets the dynamic RedHat group
  hosts: os_RedHat
  gather_facts: false
  tasks:
    - name: Report group membership
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }} is in group os_RedHat"

- name: Play that targets the dynamic Debian group
  hosts: os_Debian
  gather_facts: false
  tasks:
    - name: Report group membership
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }} is in group os_Debian"

Order matters in the files list, because the lookup tries each entry in turn and the first match wins. For an Oracle Linux 10 host that means:

  1. vars/OracleLinux-10.yml (distribution plus major version)
  2. vars/OracleLinux.yml (distribution)
  3. vars/RedHat.yml (os_family)
  4. vars/default.yml

That gives you a release override, a distro override and a family default without a single when: line. Arch is why default.yml has to exist. Its major version is NA, so the first candidate becomes Archlinux-NA.yml, and without a match further down the list the lookup fails the task. Create the vars files, one per family plus the Oracle Linux 10 override:

printf 'web_package: httpd\nweb_service: httpd\n' > vars/RedHat.yml
printf 'web_package: httpd\nweb_service: httpd\nepel_release_package: oracle-epel-release-el10\n' > vars/OracleLinux-10.yml
printf 'web_package: apache2\nweb_service: apache2\n' > vars/Debian.yml
printf 'web_package: apache\nweb_service: httpd\n' > vars/Archlinux.yml
printf 'web_package: apache2\nweb_service: apache2\n' > vars/Alpine.yml
printf 'web_package: apache2\nweb_service: apache2\n' > vars/Suse.yml
printf 'web_package: apache24\nweb_service: apache24\n' > vars/FreeBSD.yml
printf 'web_package: httpd\nweb_service: httpd\n' > vars/default.yml

Run the playbook from the project directory:

ansible-playbook -i inventory.ini os-branching.yml

With Oracle Linux 10 on node01, the run gave this output (the two interpreter discovery warnings are removed):

PLAY [Branch on OS facts] ******************************************************

TASK [Gathering Facts] *********************************************************
ok: [dev01]
ok: [node01]

TASK [Show the facts this playbook branches on] ********************************
ok: [node01] => {
    "msg": "node01: os_family=RedHat distribution=OracleLinux major=10 file_variety=OracleLinux pkg_mgr=dnf"
}
ok: [dev01] => {
    "msg": "dev01: os_family=Debian distribution=Ubuntu major=26 file_variety=Debian pkg_mgr=apt"
}

TASK [Load the most specific vars file that exists] ****************************
ok: [node01]
ok: [dev01]

TASK [Show which vars file won and the package name it sets] *******************
ok: [node01] => {
    "msg": "OracleLinux-10.yml -> install httpd, manage httpd"
}
ok: [dev01] => {
    "msg": "Debian.yml -> install apache2, manage apache2"
}

TASK [Runs only on the RedHat family] ******************************************
ok: [node01] => {
    "msg": "RedHat family host, package manager dnf"
}
skipping: [dev01]

TASK [Runs only on Debian family] **********************************************
skipping: [node01]
ok: [dev01] => {
    "msg": "Debian family host: Ubuntu 26.04"
}

TASK [Runs only on Oracle Linux 10] ********************************************
skipping: [dev01]
ok: [node01] => {
    "msg": "Oracle Linux 10, EPEL comes from oracle-epel-release-el10"
}

TASK [Group hosts by OS family] ************************************************
changed: [node01]
changed: [dev01]

PLAY [Play that targets the dynamic RedHat group] ******************************

TASK [Report group membership] *************************************************
ok: [node01] => {
    "msg": "node01 is in group os_RedHat"
}

PLAY [Play that targets the dynamic Debian group] ******************************

TASK [Report group membership] *************************************************
ok: [dev01] => {
    "msg": "dev01 is in group os_Debian"
}

PLAY RECAP *********************************************************************
dev01                      : ok=7    changed=1    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0
node01                     : ok=8    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   

Each host took the branch its facts point to, which is what you want from a mixed inventory. node01 loaded the release-specific OracleLinux-10.yml, and the controller fell through to the family file Debian.yml because there’s no Ubuntu-26.yml or Ubuntu.yml. The Oracle Linux 10 task, which needs both conditions in its when: list to be true, ran on node01 only. The single changed per host is group_by, which always reports changed when it adds a host to a group, so you should not read it as a change on the target. The screenshot below is an excerpt of the filter command and the run, with some tasks trimmed:

Ansible setup module distribution facts and a playbook branching on os_family on Ubuntu 26.04

For the second run we swapped node01 to Arch Linux, which belongs to neither the RedHat nor the Debian family. This is the second half of that run:

TASK [Show which vars file won and the package name it sets] *******************
ok: [node01] => {
    "msg": "Archlinux.yml -> install apache, manage httpd"
}
ok: [dev01] => {
    "msg": "Debian.yml -> install apache2, manage apache2"
}

TASK [Runs only on the RedHat family] ******************************************
skipping: [node01]
skipping: [dev01]

TASK [Runs only on Debian family] **********************************************
skipping: [node01]
ok: [dev01] => {
    "msg": "Debian family host: Ubuntu 26.04"
}

TASK [Runs only on Oracle Linux 10] ********************************************
skipping: [node01]
skipping: [dev01]

TASK [Group hosts by OS family] ************************************************
[WARNING]: Could not match supplied host pattern, ignoring: os_RedHat
changed: [node01]
changed: [dev01]

PLAY [Play that targets the dynamic RedHat group] ******************************
skipping: no hosts matched

PLAY [Play that targets the dynamic Debian group] ******************************

TASK [Report group membership] *************************************************
ok: [dev01] => {
    "msg": "dev01 is in group os_Debian"
}

PLAY RECAP *********************************************************************
dev01                      : ok=7    changed=1    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0
node01                     : ok=5    changed=1    unreachable=0    failed=0    skipped=3    rescued=0    ignored=0   

The Could not match supplied host pattern, ignoring: os_RedHat warning is harmless here: no host landed in that group, so its play printed skipping: no hosts matched and the run still finished with zero failures.

node01 joined a group named os_Archlinux that no play targets, and that’s the gap to watch. A family you did not plan for does not fail. It just gets nothing. We’d keep the vars-file lookup as the main mechanism, since every host gets at least default.yml, and save when: and group_by plays for work that really is family-only. For the other conditional forms, the conditionals and loops guide picks up where this playbook stops.

The same first_found list also picks whole task files when the per-OS difference is more than a package name. We tested that on the Ubuntu 26.04 controller with a tasks/ directory holding a Debian.yml but no Ubuntu.yml. The second task prints the 'NA' | int result from the version section:

- hosts: localhost
  gather_facts: true
  tasks:
    - ansible.builtin.include_tasks: "{{ lookup('ansible.builtin.first_found', params) }}"
      vars:
        params:
          files:
            - "{{ ansible_facts['distribution'] }}.yml"
            - "{{ ansible_facts['os_family'] }}.yml"
            - default.yml
          paths:
            - tasks
    - ansible.builtin.debug:
        msg: "NA as int = {{ 'NA' | int }}"

The run included the family file because the distribution file was missing, then printed NA as int = 0, and the recap showed ok=4 failed=0. This is the line that confirms which file was picked:

included: /home/josphat/anstest/tasks/Debian.yml for localhost

Keep a default.yml in tasks/ for the same reason it sits in vars/: a host with an unplanned family then runs safe default tasks instead of failing the lookup.

Keep reading

OpenCode CLI Cheat Sheet: Commands, Sessions and Fixes (PDF) AI OpenCode CLI Cheat Sheet: Commands, Sessions and Fixes (PDF) Best Mini PC for Home Assistant: 4 Picks for HAOS and Proxmox Automation Best Mini PC for Home Assistant: 4 Picks for HAOS and Proxmox Ansible + Proxmox: Automated VM Management Ansible Ansible + Proxmox: Automated VM Management Best Weather Stations for Home Assistant: Ecowitt, Tempest, Davis Automation Best Weather Stations for Home Assistant: Ecowitt, Tempest, Davis Best Water Leak Sensors and Auto Shutoff Valves for Home Assistant Automation Best Water Leak Sensors and Auto Shutoff Valves for Home Assistant How To Provision Virtual Machines on KVM with Terraform Automation How To Provision Virtual Machines on KVM with Terraform

1 thought on “Ansible OS Family and Distribution Facts: Full List (2026)”

  1. Strangely for Linux Mint 6 (Faye) Debian Edition it is:
    “`
    when: ansible_facts[‘os_family’] == “LMDE”
    when: ansible_facts[‘distribution’] == “LMDE”
    “`

    Reply

Leave a Comment

Press ESC to close