Filter ansible_devices to get the first disk without partitions

My goal is to get the first disk in ansible_devices without any partitions, to format it.

Here is the output of ansible -m setup -a filter=ansible_devices all, so in this case it’s sdb but it’s not always sdb.

    "ansible_facts": {
        "ansible_devices": {
            "sda": {
                "holders": [],
                "host": "SCSI storage controller: Red Hat, Inc. Virtio SCSI",
                "links": {
                    "ids": [
                        "scsi-0QEMU_QEMU_HARDDISK_drive-scsi0"
                    ],
                    "labels": [],
                    "masters": [],
                    "uuids": []
                },
                "model": "QEMU HARDDISK",
                "partitions": {
                    "sda1": {
                        "holders": [],
                        "links": {
                            "ids": [
                                "scsi-0QEMU_QEMU_HARDDISK_drive-scsi0-part1"
                            ],
                            "labels": [],
                            "masters": [],
                            "uuids": [
                                "58dd5456-f0e0-45ce-a7fe-028c84332cb0"
                            ]
                        },
                        "sectors": 104595423,
                        "sectorsize": 512,
                        "size": "49.87 GB",
                        "start": "262144",
                        "uuid": "58dd5456-f0e0-45ce-a7fe-028c84332cb0"
                    },
                    "sda14": {
                        "holders": [],
                        "links": {
                            "ids": [
                                "scsi-0QEMU_QEMU_HARDDISK_drive-scsi0-part14"
                            ],
                            "labels": [],
                            "masters": [],
                            "uuids": []
                        },
                        "sectors": 6144,
                        "sectorsize": 512,
                        "size": "3.00 MB",
                        "start": "2048",
                        "uuid": null
                    },
                    "sda15": {
                        "holders": [],
                        "links": {
                            "ids": [
                                "scsi-0QEMU_QEMU_HARDDISK_drive-scsi0-part15"
                            ],
                            "labels": [],
                            "masters": [],
                            "uuids": [
                                "7D87-0B8D"
                            ]
                        },
                        "sectors": 253952,
                        "sectorsize": 512,
                        "size": "124.00 MB",
                        "start": "8192",
                        "uuid": "7D87-0B8D"
                    }
                },
                "removable": "0",
                "rotational": "1",
                "sas_address": null,
                "sas_device_handle": null,
                "scheduler_mode": "none",
                "sectors": 104857600,
                "sectorsize": "512",
                "size": "50.00 GB",
                "support_discard": "4096",
                "vendor": "QEMU",
                "virtual": 1
            },
            "sdb": {
                "holders": [],
                "host": "SCSI storage controller: Red Hat, Inc. Virtio SCSI",
                "links": {
                    "ids": [
                        "scsi-0QEMU_QEMU_HARDDISK_drive-scsi1"
                    ],
                    "labels": [],
                    "masters": [],
                    "uuids": []
                },
                "model": "QEMU HARDDISK",
                "partitions": {},
                "removable": "0",
                "rotational": "1",
                "sas_address": null,
                "sas_device_handle": null,
                "scheduler_mode": "none",
                "sectors": 629145600,
                "sectorsize": "512",
                "size": "300.00 GB",
                "support_discard": "4096",
                "vendor": "QEMU",
                "virtual": 1
            },
            "sr0": {
                "holders": [],
                "host": "SATA controller: Intel Corporation 82801IR/IO/IH (ICH9R/DO/DH) 6 port SATA Controller [AHCI mode] (rev 02)",
                "links": {
                    "ids": [
                        "ata-QEMU_DVD-ROM_QM00003"
                    ],
                    "labels": [
                        "cidata"
                    ],
                    "masters": [],
                    "uuids": [
                        "2025-09-01-15-51-07-00"
                    ]
                },
                "model": "QEMU DVD-ROM",
                "partitions": {},
                "removable": "1",
                "rotational": "0",
                "sas_address": null,
                "sas_device_handle": null,
                "scheduler_mode": "mq-deadline",
                "sectors": 2048,
                "sectorsize": "2048",
                "size": "4.00 MB",
                "support_discard": "0",
                "vendor": "QEMU",
                "virtual": 1
            }
        }
    },
    "changed": false
}

I’ve been banging my head on this problem with some filters and I can get the item in the ansible_devices dict that matches, but the name of the disk is not part of the dict keys, so I don’t know how to go back one step and get the actual device name from my match.

- name: Find the first unformatted disk that is not the root device
  ansible.builtin.set_fact:
    data_disk: >
      {{
        ansible_devices.keys()
        | select('match', '^[sv]d[a-z]$')
        | map('extract', ansible_devices)
        | selectattr('partitions', 'eq', {})
        | first
      }}

- name: Debug
  debug:
    msg: "{{ data_disk }}"

The above play would produce this output.

Debug...       
  nfs01 ok: {
    "changed": false,
    "msg": {              
        "holders": [], 
        "host": "SCSI storage controller: Red Hat, Inc. Virtio SCSI",
        "links": {  
            "ids": [
                "scsi-0QEMU_QEMU_HARDDISK_drive-scsi1"
            ],        
            "labels": [],
            "masters": [],              
            "uuids": []        
        },     
        "model": "QEMU HARDDISK",
        "partitions": {},
        "removable": "0",        
        "rotational": "1",
        "sas_address": null,
        "sas_device_handle": null,
        "scheduler_mode": "none",
        "sectors": 629145600,     
        "sectorsize": "512",
        "size": "300.00 GB",
        "support_discard": "4096",
        "vendor": "QEMU",
        "virtual": 1  
    }          
}              

You’re very close. Try this:

- name: Find the first unformatted disk that is not the root device
  ansible.builtin.set_fact:
    data_disk: >
      {{
        [ansible_devices
         | dict2items
         | selectattr('value.partitions', 'eq', {})
         | first
        ] | items2dict
      }}

The filter dict2items takes a dictionary and transforms it into a list of dictionaries, with each having a key and value keys. Then your selectattr() checks value.partitions instead of partitions like it was doing before. Then the first filter gets the one you wanted. All of that gets wrapped in […] so that items2dict (which expects a list of dicts rather than the single dict) can undo what dict2items did.

Edit: I see I left out the select('match', '^[sv]d[a-z]$') part. You should be able to add that back in easily enough. I started to, but didn’t want to detract from the salient part of this answer, which is that a dict2items | <do some stuff> | items2dict pattern is really handy when you need to filter dicts and retain the keys of the matching values.

1 Like

Thank you! You definitely helped me solve it but I’m not sure I fully understand what is going on. Or of I’m doing it right. But it is working now.

Maybe it could be done in a nicer or more robust way?

- name: Find the first unformatted disk that is not the root device
  ansible.builtin.set_fact:
    disk_info: >
      {{
        [ansible_devices
          | dict2items
          | selectattr('value.partitions', 'eq', {})
          | first
        ] | items2dict
      }}

- name: Set data_disk value
  ansible.builtin.set_fact:
    data_disk: "/dev/{{ disk_info.keys() | first }}"

- name: Create a filesystem on the data disk
  community.general.filesystem:
    fstype: xfs
    dev: "{{ data_disk }}"

If I fully understood this solution I’d probably keep my select match to ensure I only match with valid disk names. But I just couldn’t sort it out.

Ah nevermind, I figured it out, was so simple.

- name: Find the first unformatted disk that is not the root device
  ansible.builtin.set_fact:
    disk_info: >
      {{
        [ansible_devices
          | dict2items
          | selectattr('key', 'match', '^[sv]d[a-z]$')
          | selectattr('value.partitions', 'eq', {})
          | first
        ] | items2dict
      }}

Now it’s just what I wanted.

This is long-winded, but I often do this with long or confusing pipelines. Run with -v to see what the set_facts are doing. (I dropped the debug steps.)

- name: Break it down | 1 | dict2items
  ansible.builtin.set_fact:
    data_disk: >
      {{
        ansible_devices
         | dict2items
      }}

- name: Break it down | 2 | selectattr acceptable keys
  ansible.builtin.set_fact:
    data_disk: >
      {{
        ansible_devices
         | dict2items
         | selectattr('key', 'match', '^[sv]d[a-z]$')
      }}

- name: Break it down | 3 | selectattr empty partitions
  ansible.builtin.set_fact:
    data_disk: >
      {{
        ansible_devices
         | dict2items
         | selectattr('key', 'match', '^[sv]d[a-z]$')
         | selectattr('value.partitions', 'eq', {})
      }}

- name: Break it down | 4,5 | first in a one-element list
  ansible.builtin.set_fact:
    data_disk: >
      {{
        [ansible_devices
         | dict2items
         | selectattr('key', 'match', '^[sv]d[a-z]$')
         | selectattr('value.partitions', 'eq', {})
         | first
        ]
      }}

- name: Break it down | 6 | items2dict
  ansible.builtin.set_fact:
    data_disk: >
      {{
        [ansible_devices
         | dict2items
         | selectattr('key', 'match', '^[sv]d[a-z]$')
         | selectattr('value.partitions', 'eq', {})
         | first
        ] | items2dict
      }}

- name: But wait... you only wanted the key anyway
  ansible.builtin.set_fact:
    data_disk: >
      {{
        (ansible_devices
         | dict2items
         | selectattr('key', 'match', '^[sv]d[a-z]$')
         | selectattr('value.partitions', 'eq', {})
         | first
        ).key
      }}