Bash and Linux

Find a New Unmounted Disk

mediumDisk and storage

Problem statement

Read saved lsblk output and find disks that have no partitions and are not mounted, like a cloud volume that was just attached. Then print the steps you would run to put it to use. Adding storage to a server is a routine task, and picking the wrong disk to format is a disaster, so knowing how to read lsblk matters.

lsblk.txt (from lsblk -nr -o NAME,TYPE,SIZE,MOUNTPOINT: no header, one line per device)

TEXT
nvme0n1 disk 8G
nvme0n1p1 part 7.9G /
nvme0n1p15 part 124M /boot/efi
nvme1n1 disk 20G
nvme2n1 disk 50G
nvme2n1p1 part 50G /data
loop0 loop 64M /snap/core/1
  1. Print every disk that has no partition and no mount point.
  2. For each one, print the commands to format it and mount it at /mnt/newdisk (print them, do not run them).

Expected output:

Bash
== disks with no partitions and no mount point ==
nvme1n1 20G
== steps to use it (printed, not run) ==
sudo mkfs.ext4 /dev/nvme1n1
sudo mkdir -p /mnt/newdisk
sudo mount /dev/nvme1n1 /mnt/newdisk
echo "UUID=$(sudo blkid -s UUID -o value /dev/nvme1n1) /mnt/newdisk ext4 defaults,nofail 0 2" | sudo tee -a /etc/fstab

Hints

Hint 1: Read the file twice in awk: the first pass remembers which disks have partitions (a part whose name starts with the disk's name) or a mount point, the second pass prints the disks that have neither.

Approach

Optimal: lsblk with a two-pass awk

Covers: block devices, lsblk columns and -nr -o, disk vs partition vs filesystem vs mount, two-pass awk (NR == FNR), the safe steps to add a disk, /etc/fstab with UUIDs.

From a raw disk to a usable folder. A new disk is just empty blocks. Four layers sit on top of each other:

%%{init: {"flowchart": {"padding": 18, "nodeSpacing": 30, "rankSpacing": 40, "htmlLabels": true}, "themeVariables": {"fontSize": "18px"}}}%% flowchart TB D[("disk
nvme1n1, 20G")]:::gray --> P["partition
optional"]:::yellow P --> F["filesystem
ext4 or XFS"]:::blue F --> M["mount point
/mnt/newdisk"]:::green classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px
Layer Example Tool
disk nvme1n1, 20G attached by the cloud or hardware
partition (optional) nvme1n1p1 fdisk, parted
filesystem ext4, XFS mkfs.ext4, mkfs.xfs
mount point /data mount, /etc/fstab

Many cloud setups put the filesystem straight on the whole disk, with no partition. LVM is another option: it groups disks into a pool (a volume group) and cuts logical volumes from it, which can be grown later.

Reading lsblk. lsblk lists block devices and how they relate. Normally it draws a tree with ├─ and └─; -r (raw) drops the tree, -n drops the header, and -o NAME,TYPE,SIZE,MOUNTPOINT picks the columns. A device with no mount point simply has 3 fields instead of 4.

In the sample:

  • nvme0n1 has partitions mounted at / and /boot/efi: the system disk.
  • nvme2n1 has nvme2n1p1 mounted at /data: in use.
  • nvme1n1 has no partitions and no mount point: the new, empty disk.
  • loop0 is a loop device from a snap package, not a real disk.

Two passes in one awk. The program reads the same file twice. While NR == FNR (the line number overall equals the line number in this file), it is the first pass: it marks a disk as used when it has a mount point, and marks a partition's parent disk as used. In the second pass, it prints disks that were never marked.

The safe steps on a real server. Check, check again, then format:

%%{init: {"flowchart": {"padding": 18, "nodeSpacing": 30, "rankSpacing": 40, "htmlLabels": true}, "themeVariables": {"fontSize": "18px"}}}%% flowchart TB A(["lsblk -f: is it empty?"]):::yellow --> B(["mkfs.ext4: erases the disk"]):::red B --> C(["mount it"]):::purple C --> E(["add UUID to /etc/fstab"]):::green classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px
Bash
lsblk -f make sure the disk has no filesystem yet
sudo mkfs.ext4 /dev/nvme1n1 create a filesystem (erases the disk)
sudo mkdir -p /mnt/newdisk
sudo mount /dev/nvme1n1 /mnt/newdisk
sudo blkid /dev/nvme1n1 get its UUID for /etc/fstab

Walking through the code. The # Setup: lines only save the sample output, so skip past them. The awk finds nvme1n1, and the loop prints the commands for it with echo, so nothing is run.

Edge cases. Device names can change between reboots, especially on NVMe and in the cloud, so /etc/fstab should use UUID=... instead of /dev/nvme1n1. A disk with an old filesystem but no mount shows nothing in MOUNTPOINT too; lsblk -f shows its FSTYPE, and formatting it destroys that data.

# Setup: save sample lsblk output in a fresh temporary folder (on a server: lsblk -nr -o NAME,TYPE,SIZE,MOUNTPOINT)
cd "$(mktemp -d)"
cat > lsblk.txt << 'OUT'
nvme0n1 disk 8G
nvme0n1p1 part 7.9G /
nvme0n1p15 part 124M /boot/efi
nvme1n1 disk 20G
nvme2n1 disk 50G
nvme2n1p1 part 50G /data
loop0 loop 64M /snap/core/1
OUT

echo "== disks with no partitions and no mount point =="
new=$(awk '
  NR == FNR {                                   # first pass: mark disks that are in use
    if ($2 == "disk" && NF == 4) used[$1] = 1
    if ($2 == "part") for (d in disk) if (index($1, d) == 1) used[d] = 1
    if ($2 == "disk") disk[$1] = 1
    next
  }
  $2 == "disk" && !($1 in used) { print $1, $3 }   # second pass: report the rest
' lsblk.txt lsblk.txt)
echo "$new"

echo "== steps to use it (printed, not run) =="
echo "$new" | while read -r name size; do
  echo "sudo mkfs.ext4 /dev/$name"
  echo "sudo mkdir -p /mnt/newdisk"
  echo "sudo mount /dev/$name /mnt/newdisk"
  echo "echo \"UUID=\$(sudo blkid -s UUID -o value /dev/$name) /mnt/newdisk ext4 defaults,nofail 0 2\" | sudo tee -a /etc/fstab"
done

Interview follow-ups

  • Make the script refuse to format a disk that already has a filesystem.

    Before mkfs, check the filesystem type: fstype=$(lsblk -no FSTYPE /dev/$name). If it is not empty, print a warning and skip that disk, because formatting would erase whatever is there. blkid /dev/$name gives the same information. For extra safety, require the expected size as an argument too, so a script cannot format a 2 TB disk when you meant the new 20 GB one.

Frequently asked questions

Device names are given out in the order the kernel finds disks, which can change after a reboot, a new disk, or a move to another instance type. If /dev/nvme1n1 and /dev/nvme2n1 swap, the wrong disk is mounted at /data, or the server fails to boot. A UUID is written inside the filesystem when it is created, so it always points to the same one. Add nofail so a missing disk does not stop the boot.

LVM (Logical Volume Manager) puts one or more disks into a pool called a volume group, then lets you cut flexible logical volumes from it. You can grow a volume while it is mounted by adding a disk to the pool, which plain partitions cannot do easily. The steps are pvcreate on each disk, vgcreate for the pool, lvcreate for a volume, then mkfs and mount as usual. In the cloud, growing the volume itself is often simpler, but LVM is still common on servers with many disks.

First make sure the kernel sees the new size with lsblk. If the filesystem sits on a partition, grow the partition with sudo growpart /dev/nvme0n1 1. Then grow the filesystem: sudo resize2fs /dev/nvme0n1p1 for ext4, or sudo xfs_growfs /mountpoint for XFS. Both work while it is mounted. Check with df -h afterwards.