#!/bin/bash # ============================================================================= # machine-setup — using the whole disk # ============================================================================= # # Definitions only, like the other lib/ files. # # ── The problem this exists for ── # # Ubuntu Server's installer, left on its defaults, creates an LVM logical volume # at a fixed size and leaves the rest of the disk as free extents in the volume # group. On a 2TB drive you get a root filesystem of around 100GB and no # indication anything is wrong: `lsblk` shows the whole disk, `df` shows 100G, # and the two are never seen side by side until the day it fills. # # The same shape turns up two other ways: # # a virtual disk grown at the hypervisor or provider, where the partition still # ends where it used to # # a partition that was resized without the filesystem inside it being told # # Three layers, and any one of them can be the short one: # # disk the physical or virtual device # container the partition, or the logical volume # filesystem what df reports # # So all three are measured and reported together. Seeing them in one place is # most of the value; the fix is usually two commands once you know which layer is # short. # # ── Only ever grows ── # # Nothing here shrinks anything, and nothing here creates or deletes a partition. # ext4, xfs and btrfs all grow while mounted, so there is no unmount and no # reboot, and a failure part-way leaves a smaller filesystem on a larger # container — which is exactly the state it started in. [[ -n "${MACHINE_SETUP_DISK_LOADED:-}" ]] && return 0 MACHINE_SETUP_DISK_LOADED=1 # ----------------------------------------------------------------------------- # What is where # ----------------------------------------------------------------------------- root_device() { findmnt -no SOURCE / 2>/dev/null; } root_fstype() { findmnt -no FSTYPE / 2>/dev/null; } # Size of a block device in bytes. dev_bytes() { lsblk -bndo SIZE "$1" 2>/dev/null || echo 0; } # Bytes, formatted the way df and lsblk format them. human_bytes() { numfmt --to=iec --suffix=B --format='%.1f' "$1" 2>/dev/null || echo "${1}B"; } # Is the root filesystem on a logical volume? root_is_lvm() { [[ "$(lsblk -ndo TYPE "$(root_device)" 2>/dev/null)" == "lvm" ]]; } # The whole disk a device ultimately sits on: /dev/sda1 -> /dev/sda, and through # LVM as well, since PKNAME walks one level at a time. parent_disk() { local dev="$1" name while true; do name="$(lsblk -ndo PKNAME "$dev" 2>/dev/null)" [[ -z "$name" ]] && break dev="/dev/${name}" done echo "$dev" } # The partition immediately below a device — for LVM, the one holding the PV. backing_partition() { local dev="$1" name while [[ "$(lsblk -ndo TYPE "$dev" 2>/dev/null)" != "part" ]]; do name="$(lsblk -ndo PKNAME "$dev" 2>/dev/null)" [[ -z "$name" ]] && return 1 dev="/dev/${name}" done echo "$dev" } # Split /dev/sda1 into "/dev/sda 1" — growpart wants them as separate arguments. # The digits come off the end because that is where a partition number is, on # /dev/sda1 and /dev/nvme0n1p2 alike. partition_parts() { local part="$1" num disk num="${part##*[!0-9]}" disk="${part%"$num"}" disk="${disk%p}" # nvme0n1p2 -> nvme0n1 echo "$disk $num" } # ----------------------------------------------------------------------------- # Sizes of the three layers # ----------------------------------------------------------------------------- # What the filesystem itself believes it is, which is the number df reports and # the only one of the three that is asked of the filesystem rather than the # kernel's block layer. fs_bytes() { local dev="$1" case "$(root_fstype)" in ext2 | ext3 | ext4) local count size count="$(tune2fs -l "$dev" 2>/dev/null | awk -F: '/^Block count:/ { gsub(/ /, "", $2); print $2 }')" size="$(tune2fs -l "$dev" 2>/dev/null | awk -F: '/^Block size:/ { gsub(/ /, "", $2); print $2 }')" [[ -n "$count" && -n "$size" ]] && echo $((count * size)) || echo 0 ;; xfs | btrfs) # Both report through the mount rather than the device. echo $(($(findmnt -bno SIZE / 2>/dev/null || echo 0))) ;; *) echo 0 ;; esac } # Unallocated extents in the volume group behind root. This is the Ubuntu # installer case, and the one that is invisible without asking LVM directly. vg_free_bytes() { local vg command -v vgs &>/dev/null || { echo 0 return } vg="$(lvs --noheadings -o vg_name "$(root_device)" 2>/dev/null | tr -d ' ')" [[ -z "$vg" ]] && { echo 0 return } vgs --noheadings --nosuffix --units b -o vg_free "$vg" 2>/dev/null | tr -d ' ' || echo 0 } # ----------------------------------------------------------------------------- # Can anything be reclaimed? # ----------------------------------------------------------------------------- # growpart answers this better than arithmetic on sector counts: it exits 0 when # it would change something and 1 with NOCHANGE when the partition already # reaches the end of the disk. Needs cloud-guest-utils, which is not installed by # default on every image. partition_can_grow() { local part="$1" disk num command -v growpart &>/dev/null || return 1 read -r disk num <<<"$(partition_parts "$part")" growpart --dry-run "$disk" "$num" &>/dev/null } ensure_growpart() { command -v growpart &>/dev/null && return 0 info " installing cloud-guest-utils, which provides growpart" pkg_install_now cloud-guest-utils } # ----------------------------------------------------------------------------- # Growing # ----------------------------------------------------------------------------- grow_partition() { local part="$1" disk num read -r disk num <<<"$(partition_parts "$part")" growpart "$disk" "$num" } # Tell LVM the partition under the physical volume got bigger. grow_pv() { pvresize "$1"; } # Take every free extent in the volume group. grow_lv() { lvextend -l +100%FREE "$(root_device)"; } # Grow the filesystem into whatever room it now has. All three do this online, so # the root filesystem is grown while it is mounted and in use. grow_fs() { case "$(root_fstype)" in ext2 | ext3 | ext4) resize2fs "$(root_device)" ;; xfs) xfs_growfs / ;; btrfs) btrfs filesystem resize max / ;; *) warn "do not know how to grow a $(root_fstype) filesystem" return 1 ;; esac }