This repository holds scripts to create and provision images and explores A/B update scenarios using RAUC.
For this first iteration, we concentrate on a system with A/B setup for ESP and root filesystem based on an upstream provided installation tarball.
Running
./src/create_image.sh
should create all required files (but this will take quite some time and it is advisable to run this on a sufficiently fast machine!).
To allow booting and modification of the aarch64
based target OS as an
unprivileged user, we make use of QEMU's
qemu-system-aarch64
, which can be run on an x86_64
based host.
Various images are created using libguestfs (which
also relies on QEMU) as part of the build process.
For more details, refer to src/create_image.sh.
We build the following images:
- a build image, containing an ESP and a root filesystem into which we boot using an EFI based mechanism
- a build data image, used to build additional software in and install from (this is mounted using the booted build image in a separate step)
- a root image, which contains only the root filesystem (derived from *build image)
- a boot image, which contains only the ESP (derived from *build image)
- an initial installation image, which contains a partition two times the size of the boot image, used for the ESP A/B setup (based on boot image) and two partitions, used for the root filesystem A/B setup (based on root image). The partitions use default Partition type GUIDs which allows systemd to automatically detect and mount them correctly.
- a signed RAUC update bundle, which contains the boot image and the root image
With the above we are able to provision an SD card using the initial installation image and install an update to the running system using the signed RAUC update bundle.
RAUC can interact with a set of bootloaders, but all of this requires customization. We loosely follow the documentation on integration with u-boot and use multiple slots in a setup that allows us to not only update the root filesystem, but also the bootloader partition itself.
A public key infrastructure (PKI) is required for RAUC to sign and validate update bundles, which can be installed on a target system.
On the target system an /etc/rauc/system.conf (see documentation) defines the compatible updates, the bootloader, the bundle format, the keyring path (for verifying the update bundle) and the slots as well as their configuration.
Using the following on the target system:
rauc install <name of bundle>
we are able to update the system using a RAUC update bundle. A subsequent reboot uses the new image.
A custom u-boot build and boot script (see custom_build/uboot-raspberrypi400/boot.txt) is required for u-boot to store its environment data (aka. it's state) in a custom location and boot in a A/B slot fashion. The customization is done using a package (see custom_build/uboot-raspberrypi400/) which is built and installed in a virtual environment.
The u-boot build configuration relies on the aarch64 based raspberrypi
preset
configuration and uses the following additional options:
CONFIG_SYS_REDUNDAND_ENVIRONMENT=y
to store the u-boot environment redundantlyCONFIG_SYS_MMC_ENV_DEV=0
to define the MMC device on which to store the environmentCONFIG_SYS_MMC_ENV_PART=0
to define the MMC partition on which to store the environmentCONFIG_ENV_SIZE=0x8000
to define 32KiB sized environment sizeCONFIG_ENV_OFFSET=0x100000
to define the offset for the first environment storage locationCONFIG_ENV_OFFSET_REDUND=0x200000
to define the offset for the 2nd environment storage locationCONFIG_ENV_IS_IN_MMC=y
to define the storage location of the environment (on MMC) - the default isCONFIG_ENV_IS_IN_FAT=y
, which stores in a FAT partition, but does neither allow redundancy nor configuration from userspace
In the target system a custom /etc/fw_env.config
(see
rootfs/etc/fw_env.config) ensures, that
the u-boot tools fw_setenv
and fw_printenv
can set and print the u-boot
environment as well, which is required for u-boot to know which of the rootfs
partitions to boot into.
Once the system boots and u-boot has chosen which of the slots to boot from and
we have left early userspace, systemd attempts to reach graphical.target
by
default. On the way multi-user.target
is passed, which pulls in
rauc-mark-good.service
(a service to mark the current slot as "good").
The service requires the custom app.target
, which itself pulls in
app.service
(a dummy service that just succeeds in execution and serves as a
placeholder for an actual target application that we want to boot into) and
orders before it.
If app.target
fails, systemd reboots the system and rauc-mark-good.service
is never reached.
This means: If app.target
fails, we will return to the previous boot slot
after three failed attempts, as RAUC never marks the new slot as good.
Only if the target application(s) run successfully we are using the new slot.
- add slot for all applications (e.g.
appfs
) in which images for portable services are supplied, to allow updates separate from (but not without!) the bootloader and rootfs. - add data partition for user data
- add build and signing infrastructure for custom packages
- create service that calls
rauc status mark-good
- improve the PKI setup and store keys in a safe place
- load kernel image, dtbs, and initramfs from rootfs slot and only use the ESP slot for u-boot updates to improve robustness
- investigate and implement the use of a RAUC rescue slot to improve robustness
- investigate and implement system for automatic RAUC update bundle consumption (e.g. using a custom service polling for files on a dedicated partition or downloading images from the internet when connection is available)
- investigate over-the-air updates using casync/desync
- host custom packages in a repository which is used to install packages during creation of images instead of building them from scratch every time.
All code is licensed under the terms of the GPL-3.0-or-later (see LICENSE file).