The official Raspian stretch image is provided as an img file with a DOS/MBR boot sector and 2 partitions, a boot partition
/boot
and the rootfs mounted at/
We can create a script which allows switching between the filesystem which is mounted at
/
and an alternative recovery root partion and provide tools on the recovery partition to reset the main root partition back to factory state.And we can do the reset over ssh which makes it super easy to automate configuration on the Pi!
This article is a follow up to the previous article and explains the details on how to create a factory restore partition for a raspberry pi. Checkout the repo from here to get the files.
Once a Pi is booted with this modified image, it has 3 partitions rather than 2,
and has a /boot/factory_reset
command which will trigger a factory reset. For
example if you login to the Pi with the modified recovery image and issue the
following commands;
$ ssh pi@raspberrypi.local
pi@raspberrypi:~ $ sudo su -
root@raspberrypi:~# /boot/factory_reset --reset
factory restore script
resetting
rebooting...
Connection to raspberrypi.local closed by remote host.
Connection to raspberrypi.local closed.
The Pi will reset itself…
And if you wait 10 minutes or so… It will replace the /
file system with a
pristine copy of raspbian (and enabled ssh access at the same time), and you will
be able to login again to fresh Raspberry Pi over ssh;
$ ssh pi@raspberrypi.local
Warning: Permanently added 'raspberrypi.local,192.168.0.23' (ECDSA) to the list of known hosts.
pi@raspberrypi.locals password:
Linux raspberrypi 4.9.80+ #1098 Fri Mar 9 18:51:28 GMT 2018 armv6l
SSH is enabled and the default password for the 'pi' user has not been changed.
This is a security risk - please login as the 'pi' user and type 'passwd' to set a new password.
pi@raspberrypi:~ $
Offical Raspian Images
The official raspian stretch lite image is about 1.7GB and contains 2 partitions.
The /boot
partition containing the kernal and other stuff, and the /
rootfs
which contains the OS.
You can see that information using the parted
tool
$ sudo parted 2018-03-13-raspbian-stretch-lite.img print
Model: (file)
Disk 2018-03-13-raspbian-stretch-lite.img: 1858MB
Sector size (logical/physical): 512B/512B
Partition Table: msdos
Disk Flags:
Number Start End Size Type File system Flags
1 4194kB 48.0MB 43.8MB primary fat32 lba
2 50.3MB 1858MB 1808MB primary ext4
The /boot/cmdline.txt
on the Pi contains the boot information with parameters
including an init
script and the root
partition to boot from;
$ cat /boot/cmdline.txt
dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=PARTUUID=cb889539-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait init=/usr/lib/raspi-config/init_resize.sh
Working around the Init root partition resizer
The init script /usr/lib/raspi-config/init_resize.sh is run on the first boot, and resizes the last partition to fill the sdcard. In order to work with the resizer script, the idea is to add the recovery partition after the boot partition and before the main rootfs.
If we were to add the recovery partition at the end, the init resizer script
would expand the recovery partition to fill the disk, rather than the live /
Recovery partition
So basically we want to insert the recovery partition here…
Number Start End Size Type File system Flags
1 4194kB 48.0MB 43.8MB primary fat32 lba
<<<< ------------ INSERT RECOVERY PARTITION HERE
2 50.3MB 1858MB 1808MB primary ext4
Selecting the root=
rootfs or recovery partition on boot
The root=
option in /boot/cmdline.txt
selects the root filesystem which is
then mounted and booted into the OS.
root=PARTUUID=cb889539-02
By default that points at the vanilla root. If we add another partition, this value needs updating. It also needs changing in order to boot from the recovery partition.
While coming up with this script I had some issues that I could not get the Pi to use the disk UUID to boot, it would only work with the PARTUUID. However the resizer.sh script resets the PARTUUID during its resizing process which complicates matters.
Generating UUID and PARTUUID for the disks
As we are going to be cloning a disk, we have a problem of a duplicate UUID for that disk. We also need to have a PARTUUID for the cmdline.txt file. So we generate random values for this as a first step;
UUID_RESTORE=$(uuidgen)
UUID_ROOTFS=$(uuidgen)
# partuuid seems to get reset by resize.sh, however UUID doesn't seem to work
PARTUUID=$(tr -dc 'a-f0-9' < /dev/urandom 2>/dev/null | head -c8)
Creating a blank disk img
file to work with;
This is the image that will be written to the sdcard. This is a way to initialize
a new img
file of a specific size. In this case 2000 x 4M
. Currently that is
fixed, but I would like to inspect those values from the source image as a future
feature.
sudo dd if=/dev/zero bs=4M count=2000 > 2018-03-13-raspbian-stretch-lite.restore.img
This creates an empty file with zeros. We need to add a partition table to that;
$ sudo sfdisk 2018-03-13-raspbian-stretch-lite.restore.img <<EOL
label: dos
label-id: 0x${PARTUUID}
unit: sectors
2018-03-13-raspbian-stretch-lite.restore.img1 : start=8192, size=85611, type=c
2018-03-13-raspbian-stretch-lite.restore.img2 : start=98304, size=8388608, type=83
2018-03-13-raspbian-stretch-lite.restore.img3 : start=8486920, size=4194296, type=83
EOL
These values are hard coded to fit the raspian stretch lite image, and it’s on the TODO list to inspect the sizes from the source image file.
We can inspect what was created using the follwing command
sudo fdisk -lu 2018-03-13-raspbian-stretch-lite.restore.img
Disk 2018-03-13-raspbian-stretch-lite.restore.img: 7.8 GiB, 8388608000 bytes, 16384000 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x2228528e
Device Boot Start End Sectors Size Id Type
2018-03-13-raspbian-stretch-lite.restore.img1 8192 93802 85611 41.8M c W95 FAT32 (LBA)
2018-03-13-raspbian-stretch-lite.restore.img2 98304 8486911 8388608 4G 83 Linux
2018-03-13-raspbian-stretch-lite.restore.img3 8486920 12681215 4194296 2G 83 Linux
We need to make some changes to the paritions in the image, and copy the filesystems from the source image, so we mount them on the loopback device using the following commands;
sudo losetup -v -f 2018-03-13-raspbian-stretch-lite.restore.img
sudo partx -v --add /dev/loop0
Now the partitions from the img file are available as loopback devices so we can work with them;
$ sudo cat /proc/partitions
major minor #blocks name
8 0 234431064 sda
8 1 512000 sda1
8 2 233917440 sda2
8 16 976762584 sdb
11 0 2 sr0
253 0 190136320 dm-0
253 1 16515072 dm-1
253 2 976760832 dm-2
253 3 27262976 dm-3
7 0 8192000 loop0
259 0 42805 loop0p1
259 1 4194304 loop0p2
259 2 2097148 loop0p3
The next step is to mount the source image on loopback device similar to the previous step
sudo losetup --show -f -P 2018-03-13-raspbian-stretch-lite.live.img
cat /proc/partitions | grep loop
sudo losetup -a | grep loop
7 0 8192000 loop0
259 0 42805 loop0p1
259 1 4194304 loop0p2
259 2 2097148 loop0p3
7 1 1814528 loop1
259 3 42805 loop1p1
259 4 1765376 loop1p2
Now we can copy the partitions from the source image to the recovery image;
dd if=/dev/loop1p1 of=/dev/loop0p1 bs=4M
# initially recovery and rootfs partitions are the same
dd if=/dev/loop1p2 of=/dev/loop0p2 bs=4M
dd if=/dev/loop1p2 of=/dev/loop0p3 bs=4M
We now write the UUIDs for the disks so they are unique going forward;
# mkdosfs -i ${UUID_BOOT} /dev/loop0p1
tune2fs /dev/loop0p2 -U ${UUID_RESTORE}
# change the LABEL= on the recovery partition
e2label /dev/loop0p2 recoveryfs
tune2fs /dev/loop0p3 -U ${UUID_ROOTFS}
The next thing is that the recovery partition is 4G from the sdisk
command
above, but the filesystem that we just wrote is only 1.7 GB
. So we need to
resize that filesystem;
sudo resize2fs /dev/loop0p2
resize2fs 1.43.5 (04-Aug-2017)
Resizing the filesystem on /dev/loop0p2 to 1048576 (4k) blocks.
The filesystem on /dev/loop0p2 is now 1048576 (4k) blocks long.
We can now mount those partions and make the changes for factory reset;
mkdir -p mnt/restore_boot
mkdir -p mnt/restore_recovery
mkdir -p mnt/restore_rootfs
mount /dev/loop0p1 mnt/restore_boot
mount /dev/loop0p2 mnt/restore_recovery
mount /dev/loop0p3 mnt/restore_rootfs
The first thing is that we want the Pi to boot off the 3rd partition, rather
than the 2nd, do we need to modify /boot/cmdline.txt
in the restore image;
(and also enable ssh…)
cat << EOF > mnt/restore_boot/cmdline.txt
dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=PARTUUID=${PARTUUID}-03 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait init=/usr/lib/raspi-config/init_resize.sh
EOF
# enable ssh on the image
touch mnt/restore_boot/ssh
The next thing is to make backup of the cmdline.txt
so we can put it back after
recovery/reset;
cp mnt/restore_boot/cmdline.txt mnt/restore_boot/cmdline.txt_original
So the next step we create a /boot/cmdline.txt
which is used during recovery.
However as the PARTUUID is reset during resizing. we include a token
which is replaced at runtime by our own script, and we tell the Pi to boot our
recovery script when its reset init=/usr/lib/raspi-config/init_resize2.sh
(there is probably a
simpler way to do this) But I couldn’t get root=UUID=xxxxxxxx
or LABEL=recovery
type booting to work. So I seem to be stuck with manipulating the root=PARTUUID=
option.
cat << EOF > mnt/restore_boot/cmdline.txt_recovery
dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=PARTUUID=XXXYYYXXX rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait init=/usr/lib/raspi-config/init_resize2.sh
EOF
The next step is to write out the script that actually resets the pi. This is on the recovery partition root fs.
We also need to copy the recovery image (/opt/recovery.img
) to the restore partition, but we do that
last because we want the modifications we are making to be persistent after
restoration.
Note that it also enables ssh for the restored OS.
cat << 'EOF' > mnt/restore_recovery/usr/lib/raspi-config/init_resize2.sh
#!/bin/sh
echo "factory restore script"
# echo "found recovery image, restoring it"
dd bs=4M if=/opt/recovery.img of=/dev/mmcblk0p3 conv=fsync status=progress
cp -f /boot/cmdline.txt_original /boot/cmdline.txt
# enable ssh on the image
touch /boot/ssh
reboot
EOF
# make the script executable
chmod +x mnt/restore_recovery/usr/lib/raspi-config/init_resize2.sh
we also need a command which triggers the recovery from the live system;
Notice that this inspects the blkid of the PARTUUID, and replaces the token
(XXXYYYXXX
) created earlier with the correct runtime PARTUUID
cat << EOF > mnt/restore_boot/factory_reset
#!/bin/bash
echo "factory restore script"
[[ "\$1" == "--reset" ]] && \
{
echo "resetting"
cp -f /boot/cmdline.txt /boot/cmdline.txt_original
cp -f /boot/cmdline.txt_recovery /boot/cmdline.txt
sed -i "s/XXXYYYXXX/\$(blkid -o export \
/dev/disk/by-uuid/${UUID_RESTORE} | \
egrep '^PARTUUID=' | cut -d'=' -f2)/g" /boot/cmdline.txt
echo "rebooting..."
reboot
exit 0
}
EOF
# make the script executable
chmod +x mnt/restore_boot/factory_reset
In addition we need to update the /etc/fstab
on each of the root partitions
so they correct mount their respective partitions;
Note: using UUID seems to work fine in OS land, but not in cmdline.txt… weird huh
# get UUID from the boot partition in the img
UUID_BOOT=$(blkid -o export /dev/loop0p1 | egrep '^UUID=' | cut -d'=' -f2)
cat << EOF > mnt/restore_rootfs/etc/fstab
proc /proc proc defaults 0 0
UUID=${UUID_BOOT} /boot vfat defaults 0 2
UUID=${UUID_ROOTFS} / ext4 defaults,noatime 0 1
EOF
cat << EOF > mnt/restore_recovery/etc/fstab
proc /proc proc defaults 0 0
UUID=${UUID_BOOT} /boot vfat defaults 0 2
UUID=${UUID_RESTORE} / ext4 defaults,noatime 0 1
EOF
Write out the rootfs image to a fisk image on the recovery partition so it can be restored. @TODO zip this…
sudo dd if=/dev/loop0p3 of=mnt/restore_recovery/opt/recovery.img bs=4M
511+1 records in
511+1 records out
2147479552 bytes (2.1 GB, 2.0 GiB) copied, 4.40628 s, 487 MB/s
Finally, unmount the filesystems and loop devices;
umount -f mnt/restore_boot
umount -f mnt/restore_rootfs
umount -f mnt/restore_recovery
losetup --detach-all
Use the following command to write the image to an sdcard;
dd bs=4M if=2018-03-13-raspbian-stretch-lite.restore.img of=/dev/sdX conv=fsync status=progress
Summary
The file 2018-03-13-raspbian-stretch-lite.restore.img
now contains
- boot partition pointing at partition 3
- recovery script at
/boot/factory_reset
- Recovery partition at partition 2
- Recovery image in
/opt/recovery.img
of recovery partition - Correct
/etc/fstab
for partition 2 and 3 - reset script at
/usr/lib/raspi-config/init_resize2.sh
of recovery partition
So write the img to a file system, and when you want to reset it, issue the following command;
root@raspberrypi:~# /boot/factory_reset --reset
factory restore script
resetting
rebooting...
Connection to raspberrypi.local closed by remote host.
Connection to raspberrypi.local closed.
Make a cup of coffee, and wait 10 minutes, and by the time your are back, the Pi will be fresh and new and ready and waiting…