266 lines
8.9 KiB
Markdown
266 lines
8.9 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
#!/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
|
|
|
|
```bash
|
|
#!/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)
|
|
|
|
```bash
|
|
#!/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
|
|
|
|
```bash
|
|
# 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)
|
|
|
|
```bash
|
|
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:
|
|
```bash
|
|
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
|