Files
hermes-skills/devops/proxmox-ve-administration/references/initramfs-auto-shrink-template.md
T

8.9 KiB

Automated ext4 Root Partition Shrink via initramfs

Templates for shrinking an ext4 root partition unattended (no console interaction). Developed on Ubuntu 24.04 (10.0.30.100), Ceph 19.2.3, kernel 6.8.0-134-generic.

Components

  1. initramfs premount script — runs e2fsck → resize2fs → sgdisk automatically
  2. initramfs hook — copies resize tools + GPT backup into initramfs
  3. GRUB rescue entry — boot target with shrink_root=1 kernel parameter
  4. grub-reboot — one-time boot selection (auto-reverts to normal boot)

File: /etc/initramfs-tools/scripts/init-premount/auto-shrink-root

#!/bin/sh
PREREQ=""
prereqs() { echo "$PREREQ"; }
case "$1" in
    prereqs) prereqs; exit 0;;
esac

. /scripts/functions

# Only run if shrink_root=1 on cmdline
grep -q "shrink_root=1" /proc/cmdline || exit 0

echo ""
echo "=========================================="
echo "  AUTO ROOT PARTITION SHRINK"
echo "  Target: sda2 224G -> 160G + sda4 64G"
echo "=========================================="
echo ""

# Safety: wait for /dev/sda to appear
for i in $(seq 1 10); do
    [ -b /dev/sda ] && break
    echo "Waiting for /dev/sda... ($i)"
    sleep 1
done
[ -b /dev/sda ] || panic "ERROR: /dev/sda not found"

# Safety: verify sda2 is ext4
blkid /dev/sda2 2>/dev/null | grep -q ext4 || panic "ERROR: sda2 is not ext4 — aborting"
echo "✓ sda2 is ext4"

# Step 1: e2fsck
echo ""
echo "[1/4] Filesystem check (e2fsck -f)..."
e2fsck -f -y /dev/sda2
rc=$?
if [ $rc -gt 1 ]; then
    echo "ERROR: e2fsck failed (exit $rc)"
    panic "e2fsck failed — dropping to shell"
fi
echo "✓ e2fsck OK (exit $rc)"

# Step 2: resize2fs
echo ""
echo "[2/4] Shrinking ext4 to 160G (resize2fs)..."
resize2fs /dev/sda2 160G
rc=$?
if [ $rc -ne 0 ]; then
    echo "ERROR: resize2fs failed (exit $rc)"
    echo "Filesystem may be inconsistent. Running e2fsck..."
    e2fsck -f -y /dev/sda2
    panic "resize2fs failed — dropping to shell"
fi
echo "✓ resize2fs OK — ext4 is now 160G"

# Step 3: Partition table — sgdisk
echo ""
echo "[3/4] Updating partition table (sgdisk)..."

# Delete partition 2
sgdisk -d 2 /dev/sda 2>&1
rc=$?
[ $rc -ne 0 ] && panic "sgdisk: failed to delete sda2"

# Recreate partition 2 (smaller, same start)
sgdisk -n 2:1050624:336594943 /dev/sda 2>&1
rc=$?
if [ $rc -ne 0 ]; then
    echo "ERROR: failed to recreate sda2 — restoring original table"
    sgdisk --load-backup=/root/gpt_backup.bin /dev/sda 2>/dev/null || true
    panic "sgdisk: failed to create sda2"
fi

# Set type Linux filesystem
sgdisk -t 2:8300 /dev/sda 2>&1

# Preserve original PARTUUID (CRITICAL — fstab uses UUID, but PARTUUID consistency is safer)
sgdisk --partition-guid=2:335FFF44-5FB5-4BEC-9B68-66E39D5961B7 /dev/sda 2>&1

# Create partition 4 in the gap (LVM)
sgdisk -n 4:336594944:470347775 /dev/sda 2>&1
rc=$?
if [ $rc -ne 0 ]; then
    echo "WARNING: Failed to create sda4 (non-fatal, continuing)"
else
    sgdisk -t 4:8E00 /dev/sda 2>&1
    echo "✓ sda4 created (LVM, ~63.8G)"
fi

echo "✓ Partition table updated"

# Step 4: Reread partition table
echo ""
echo "[4/4] Rereading partition table..."
blockdev --rereadpt /dev/sda 2>&1
sleep 2

# Verify
if [ -b /dev/sda4 ]; then
    echo "✓ /dev/sda4 exists"
else
    echo "WARNING: /dev/sda4 not visible yet (may appear after udev settle)"
fi

echo ""
echo "=========================================="
echo "  SHRINK COMPLETE"
echo "  sda2: 160G ext4 (root)"
echo "  sda4: ~63.8G (free for LVM/ceph-db)"
echo "  Continuing boot..."
echo "=========================================="
echo ""

exit 0

File: /etc/initramfs-tools/hooks/resize_tools

#!/bin/sh
PREREQ=""
prereqs() { echo "$PREREQ"; }
case "$1" in prereqs) prereqs; exit 0;; esac

# CRITICAL: Must source hook-functions for copy_exec to work
. /usr/share/initramfs-tools/hook-functions

copy_exec /usr/sbin/resize2fs
copy_exec /usr/sbin/e2fsck
copy_exec /usr/sbin/tune2fs
copy_exec /usr/sbin/sgdisk
copy_exec /usr/sbin/fdisk
copy_exec /usr/bin/blockdev
copy_exec /usr/sbin/blkid

# GPT backup for rollback (embedded in initramfs — root FS NOT mounted in premount!)
mkdir -p "${DESTDIR}/root"
cp /etc/initramfs-tools/gpt_backup.bin "${DESTDIR}/root/gpt_backup.bin"

File: /etc/grub.d/40_custom_rescue (GRUB entry)

#!/bin/sh
exec tail -n +3 "$0"
menuentry "Rescue: Shrink root (initramfs premount shell)" {
    insmod part_gpt
    insmod ext2
    set root=(hd0,gpt2)
    linux /boot/vmlinuz-6.8.0-134-generic root=/dev/sda2 ro shrink_root=1
    initrd /boot/initrd.img-6.8.0-134-generic
}

⚠️ The GRUB entry must NOT contain break=premount — that would drop to a shell instead of running the script. The shrink_root=1 parameter triggers the script, which then continues boot automatically.

Setup Sequence

# 1. Create GPT backup (for rollback)
sgdisk --backup=/root/gpt_backup.bin /dev/sda
cp /root/gpt_backup.bin /etc/initramfs-tools/gpt_backup.bin

# 2. Install premount script
cp auto-shrink-root /etc/initramfs-tools/scripts/init-premount/
chmod +x /etc/initramfs-tools/scripts/init-premount/auto-shrink-root

# 3. Install hook
cp resize_tools /etc/initramfs-tools/hooks/
chmod +x /etc/initramfs-tools/hooks/resize_tools

# 4. Install GRUB entry
cp 40_custom_rescue /etc/grub.d/
chmod +x /etc/grub.d/40_custom_rescue

# 5. Remove swap from fstab (if swap partition is being repurposed)
# Edit /etc/fstab, comment out swap line

# 6. Increase GRUB timeout temporarily (to see what happens)
sed -i 's/GRUB_TIMEOUT=.*/GRUB_TIMEOUT=10/' /etc/default/grub
sed -i 's/GRUB_DEFAULT=.*/GRUB_DEFAULT=saved/' /etc/default/grub

# 7. Rebuild GRUB + initramfs
update-grub
update-initramfs -u -k $(uname -r)

# 8. Verify script and tools are in initramfs
lsinitramfs /boot/initrd.img-$(uname -r) | grep auto-shrink
lsinitramfs /boot/initrd.img-$(uname -r) | grep -E "resize2fs|e2fsck|sgdisk|blockdev|blkid"
lsinitramfs /boot/initrd.img-$(uname -r) | grep gpt_backup

# 9. Set one-time boot to rescue entry
grub-set-default 0                           # Persistent default = normal Ubuntu
grub-reboot "Rescue: Shrink root (initramfs premount shell)"  # Next boot only

# 10. Verify
cat /boot/grub/grubenv    # saved_entry=0, next_entry=Rescue...

# 11. Reboot
reboot

Post-Reboot Verification (Remote via SSH)

ssh root@<host> 'df -h /'                    # Root should be ~160G
ssh root@<host> 'lsblk /dev/sda'             # sda4 should appear
ssh root@<host> 'pvs && vgs && lvs'          # LVM intact
ssh root@<host> 'systemctl status ceph-osd@8 ceph-osd@9'  # OSDs up

Then use sda4 for Ceph DB:

pvcreate /dev/sda4
vgcreate ceph-db-vg-new /dev/sda4
lvcreate -L 30G -n osd-9-db-new ceph-db-vg-new
# Migrate osd.9 DB from loopback to real partition...

Pitfalls (Learned the Hard Way)

Pitfall 1: initramfs hook copy_exec not found

Symptom: Hook runs but copy_exec: command not found. Tools NOT included in initramfs. Cause: Hook script doesn't source /usr/share/initramfs-tools/hook-functions. Fix: Add . /usr/share/initramfs-tools/hook-functions at the top of the hook (after the PREREQ boilerplate).

Pitfall 2: GPT backup not accessible in premount

Symptom: Script tries sgdisk --load-backup=/root/gpt_backup.bin but file not found. Cause: In premount phase, root FS is NOT mounted. /root/ in the script refers to the initramfs's /root/ directory, not the real root filesystem. Fix: Copy the GPT backup into the initramfs via the hook: cp /etc/initramfs-tools/gpt_backup.bin "${DESTDIR}/root/gpt_backup.bin".

Pitfall 3: GRUB entry with break=premount stops the boot

Symptom: Boot drops to initramfs shell, script never runs. Cause: break=premount tells the kernel to halt in premount phase for manual intervention. Fix: Use shrink_root=1 instead — the script checks /proc/cmdline for this parameter. No break= parameter.

Pitfall 4: grub-set-default persists across reboots

Symptom: Every subsequent boot goes to rescue entry. Cause: grub-set-default sets the persistent default. Fix: Use grub-reboot for one-time boot. Set grub-set-default 0 first (normal Ubuntu), then grub-reboot "Rescue..." for next boot only. grub-reboot writes next_entry which is consumed on first boot.

Pitfall 5: resize2fs changes neither UUID nor PARTUUID

Fact: resize2fs only modifies ext4 superblock metadata (block count, free block count). The filesystem UUID and partition PARTUUID remain identical. Normal boot via root=UUID=... in fstab works unchanged. sgdisk --partition-guid=2:<original> ensures PARTUUID consistency too.

Pitfall 6: Hard reset during shrink is survivable

All intermediate states are bootable:

  • After e2fsck only: FS unchanged → normal boot
  • After resize2fs only: FS is 160G on 224G partition → normal boot (ext4 handles smaller FS on larger partition)
  • After sgdisk only: Partition table updated → normal boot with 160G partition
  • next_entry is consumed regardless → saved_entry=0 (Ubuntu) takes over