Live migration of root disk with Linux


I recently had the need to update my NVMe drive and took the time to make an extensively commented script I simply give you now. The main "issue" here was that I really didn't want to reinstall my OS from scratch instead of just dding the disk. I now know that there's a bit more minutiae to it but not that much. So here it is:

#!/bin/sh
# Requires POSIX 2024 dd and util-linux (sfdisk, uuidgen, blkid, lsblk, etc...)
set -eux

[ $(id -u) -ne 0 ] && { echo "Permission denied" >&2; exit 1; }

src=$1
dst=$2

# First, we simply copy the full disk (incl. its partition table and potential bootloader)
# The conv and iflag values are necessary in case of src corruption
dd if="$src" of="$dst" bs=1M status=progress conv=sync,noerror iflag=fullblock

# Change dst's full disk GPT UUID (duplicates can confuse some kernels/applications)
sfdisk --disk-id "$dst" "$(uuidgen)"

dstparts=$(sfdisk --quiet --list "$dst" | awk 'NR > 1 {print $1}')
partidx=1
echo "$dstparts" | while IFS= read -r part
do
    # Same for the partitions, we need new PARTUUIDs
    sfdisk --part-uuid "$dst" $partidx "$(uuidgen)"
    partidx=$((partidx + 1))

    # Now, for each partition on the dst disk with a filesystem, we need to:
    # 1. Check for errors
    # 2. Change its UUID (different thing from the earlier GPT PARTUUID)
    fstype=$(lsblk -n -o FSTYPE "$part")
    case "$fstype" in
        ext[234])
            fsck "$part"
            tune2fs -U random "$part"
            ;;
        xfs)
            xfs_repair "$part"
            # Some corruption types require a `mount -o nouuid "$part" mnt/`
            # and possibly a `xfs_repair -L "$part"` at that point
            xfs_admin -U generate "$part"
            ;;
        '') ;; # FS-less partition, used to e.g host bootloader
        *)  echo "$fstype: unsupported FS type" >&2
    esac
done

# Now edit grub.cfg and fstab to use those new UUID/PARTUUIDs; you weren't
# still using unpredictable raw device paths in here, right?
#
# Protip: use `blkid -p -o value -s UUID /dev/part` to show the _up-to-date_
# UUIDs (resp. PART_ENTRY_UUID for PARTUUIDs)

NB: this works even while running the OS on the source, though I don't know if this can lead to corrupted reads (depending on the filesystems, probably).