Purpose: Create a flashable image of a drive and resize it so unused space is minimized. Created image can be flashed as a backup that contains all config files.
Application: Backups of router and pihole.
Before starting, make sure the disk to be imaged is unmounted.
-
Image the source drive:
sudo dd if=/dev/sdb of=image.img bs=4M status=progressIf space is an issue,ddcan be piped straight to gzip, but you'll need to unzip before you can manipulate the image.sudo dd if=/dev/sdb bs=4M status=progress | gzip -9 > image.img.gz -
MAKE A BACKUP OF THE IMAGE. (e.g.,
cp image.img working.img) -
Save the working image filename into an environmental variable to make things easier. `export LOOPIMAGE="working.img"
-
Mount the image using the loop device and set environmental variables so subsequent commands work when cut/paste, also show the user the partitions for a sanity check. This and subsequent commands assume that the second partition is to be resized. If this is not true, then $LOOPPART needs to be manually set.
LOOPDEV=$(sudo losetup -fP --show ${LOOPIMAGE})
lsblk "$LOOPDEV" && LOOPPART=$(lsblk -brno NAME,TYPE,START "$LOOPDEV" | awk '$2=="part"{p=$1;s=$3} END{print "/dev/"p}') && printf "\nLoop device: %s\nPartition to resize: %s\n" "$LOOPDEV" "$LOOPPART"
-
Check and repair errors:
sudo e2fsck -f ${LOOPPART} -
Find minimum file system size. This will output the size in blocks and MiB.
min=$(sudo resize2fs -P ${LOOPPART} 2>/dev/null | awk '{print $7}'); \
bsize=$(sudo dumpe2fs -h ${LOOPPART} 2>/dev/null | awk '/Block size:/ {print $3}'); \
echo "Block size: $bsize"; \
echo "Minimum blocks: $min"; \
echo "Min FS size: ${min} blocks ($((min*bsize/1024/1024)) MiB)"; \
echo "1.5x FS size: $((min*3/2)) blocks ($((min*3/2*bsize/1024/1024)) MiB)"; \
echo "2x FS size: $((min*2)) blocks ($((min*2*bsize/1024/1024)) MiB)"; \
echo "sudo resize2fs ${LOOPPART} $((min*3/2))"
-
Shrink the file system. To be safe, pick a size 1.5-2X the minimum.
resize2fsexpects blocks. Providing some extra space (1.5X) will give some leeway when restoring. -
Calculate numbers for the new partition. This sets the new end of the partition to [start of partition 2 + new filesystem size + 50–100 MiB slack]
partnum=$(echo "$LOOPPART" | grep -o '[0-9]\+$')
fsinfo=$(sudo dumpe2fs -h ${LOOPPART} 2>/dev/null | awk '/Block count:/ {count=$3} /Block size:/ {size=$3} END {print count*size}')
pstart=$(sudo parted -sm ${LOOPDEV} unit B print | awk -F: -v pn="$partnum" '$1==pn {sub(/B$/,"",$2); print $2}')
margin=$((fsinfo + 1024*1024))
end=$((pstart + margin))
printf "\n\nFS size: %s bytes \nPart start: %s bytes\nEnd+1MiB: %s bytes\nRun this:\n sudo parted %s unit B resizepart %s %sB\n" "$fsinfo" "$pstart" "$end" "$LOOPDEV" "$partnum" "$end"
Resize the partition using the recommended command.
-
Verify file system fits.
sudo e2fsck -f ${LOOPPART} -
Find the new partition end. This rounds up to the next MiB boundary to ensure full compatibility.
img=$(losetup -a | awk -v dev="$LOOPDEV" -F'[()]' '$1 ~ dev {print $2}')
sudo parted -sm ${LOOPDEV} unit B print \
| awk -v img="$img" -v dev="$LOOPDEV" -F: '$1 ~ /^[0-9]+$/ {end=$3} END {
sub(/B$/,"",end)
mib=1024*1024
rounded=int((end + mib - 1) / mib) * mib
safe=rounded + mib
printf "End: %s bytes\n", end
printf "Rounded: %d bytes (next MiB)\n", rounded
printf "Safe +1M: %d bytes\n", safe
print "\n👉 Before truncating, detach the loop device:"
printf " sudo losetup -d %s\n", dev
print "\nThen run:"
printf " sudo truncate -s %d \"%s\"\n", safe, img
}'
Dismount the loop device, then truncate using the command suggested.
- Check file system again.
sudo losetup -fP ${LOOPIMAGE}
sudo e2fsck -f ${LOOPPART}
sudo losetup -d ${LOOPDEV}
- Check image is readable
dd if=${LOOPIMAGE} of=/dev/null bs=1M status=progress
-
Zip it for additional space savings.
gzip -9 ${LOOPIMAGE} -
Remove the original image.