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…

My helpful screenshot

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

  1. boot partition pointing at partition 3
  2. recovery script at /boot/factory_reset
  3. Recovery partition at partition 2
  4. Recovery image in /opt/recovery.img of recovery partition
  5. Correct /etc/fstab for partition 2 and 3
  6. 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…