Skip to content

Convert a Debian or Raspberry Pi OS image

otapulse-convert takes a disk image of a standard Linux distribution and turns it into an OTA-Pulse image for bootloader compatibility mode. You do not need a Yocto build.

The converted image has:

  • a boot partition,
  • two root file system partitions, A and B,
  • a data partition,
  • the bootloader (GRUB or U-Boot), set up to count the boots of a fresh update and to revert it on its own,
  • the OTA-Pulse agent (soc-ota-agent) and the monitoring daemon (socmond),
  • a .otapulse artifact of its root file system, which you can sign with your key.
Input imageConfigurationStatus
Debian 12 arm64 “genericcloud” or “nocloud” (UEFI, GRUB)configs/debian_arm64_uefi_configConverted and booted under QEMU. Virtio disk (/dev/vda) only.
Raspberry Pi OS (Debian 12) arm64, U-Bootconfigs/raspberrypi/uboot/debian/raspberrypi4_bookworm_64bit_configNot run.
32-bit armhf or amd64 imagesUntested. Other boards, Ubuntu, 32-bit images and Raspberry Pi UEFI are not included.
An image with no GRUB of its own (standalone GRUB)Untested. The conversion stops with a message that the bootloader assets are not listed.
EFI secure boot, BIOS bootUntested. The GRUB environment scripts and configuration are not signed.

The generic arm64 configuration assumes a virtio disk. For a real UEFI board, copy it and set OTAPULSE_STORAGE_DEVICE_BASE to the disk of the board, for example /dev/nvme0n1p. Or set OTAPULSE_ENABLE_PARTUUID=y together with the four partition options. Change the console arguments too.

  • A Linux host with Docker. The container runs with --privileged because the conversion uses loop devices and mounts.
  • For an image of another architecture than the host (arm64 on an x86-64 host), qemu-user registered in the kernel through binfmt_misc. The tool does not change this.
  • Network access during the conversion. The tool installs the dependencies of the two packages, and parted and gdisk, from the package repositories of the input image.
  • The tool tarball otapulse-convert-<version>.tar.gz. It holds the tool and the artifact tool. It holds the soc-ota-agent and socmond packages only if it was built with them; otherwise copy the .deb files for your architecture into input/packages.
  1. Unpack the tool and check it

    Terminal window
    tar -xzf otapulse-convert-<version>.tar.gz
    cd otapulse-convert
    ./otapulse-convert --version
    ./otapulse-convert --help

    --help prints the command line. It shows the options --disk-image, --config, --overlay, --version and --help.

  2. Build the container image once

    Terminal window
    ./docker-build
  3. Put the input image in place

    Terminal window
    cp debian-12-genericcloud-arm64.raw input/

    The packages for the right architecture are read from input/packages.

  4. Write a configuration with your values

    Create input/my_config. This is a plain shell file. All options and their defaults are in configs/otapulse_convert_config.

    Terminal window
    OTAPULSE_SERVER_URL="https://<your-server>"
    OTAPULSE_DEVICE_TYPE="my-device"
    OTAPULSE_ARTIFACT_SIGNING_KEY="/otapulse-convert/input/release-private.pem"
    OTAPULSE_PANIC_TIMEOUT_S="10"

    The signing key path is the path inside the container. Put the key in input/.

  5. Convert

    Terminal window
    OTAPULSE_ARTIFACT_NAME="release-1" ./docker-otapulse-convert \
    --disk-image input/debian-12-genericcloud-arm64.raw \
    --config configs/debian_arm64_uefi_config \
    --config input/my_config

    OTAPULSE_ARTIFACT_NAME is required. The device reports it as its running software version, and different updates need different names. The conversion took about 2 minutes in our runs.

The result is in deploy/:

  • <name>.img: the disk image to flash or boot,
  • <name>.ext4: the root file system,
  • <name>.otapulse: the artifact of the root file system.

The file name is built from the input image name and the device type. The tool also prints where its debug log is. That log holds every command the tool ran.

On the Debian 12 arm64 image the result was a GPT disk of 8 GiB:

PartitionSizeUse
1128 MiBEFI system partition, mounted at /boot/efi
2 and 3about 3.9 GiB eachRoot file system A and B
4128 MiB, grown on first boot/data

The data partition grows to the end of the disk on the first boot. In our run it went from 128 MiB to 152 MiB, which was all the free space. The image’s own GRUB is kept and gets the A/B boot scripts. Every kernel line carries panic=<seconds>.

Every option has the prefix OTAPULSE_. Set your own values in a file and pass it with --config. The defaults file is always read first.

OptionDefaultMeaning
OTAPULSE_ARTIFACT_NAMEnoneRequired. Pass it in the environment. The name of the image and update.
OTAPULSE_DEVICE_TYPEthe hostname of the input imageThe device type. The agent installs an artifact only if its device type list contains this value.
OTAPULSE_SERVER_URLemptyWritten into the agent and socmond configuration at the first install of the packages. Plain URL characters only. If it is empty, edit /etc/otapulse/otapulse.conf and /etc/socmond/config.json afterwards.
OTAPULSE_ARTIFACT_SIGNING_KEYemptyRSA private key that signs the .otapulse artifact. With none, the artifact is unsigned and the tool warns. A production agent installs only signed artifacts.
OTAPULSE_PANIC_TIMEOUT_S10panic=<n> on every kernel line. The conversion fails if the value is not positive.
OTAPULSE_GRUB_KERNEL_BOOT_ARGSemptyExtra kernel arguments for the image’s own GRUB, such as the console.
OTAPULSE_MACHINE_ID_PERSISTyKeeps the machine id on the data partition across updates. Without it every update would register the device again under a new identity.
OTAPULSE_SOCMOND_WATCHDOGny enables socmond-watchdog. See below.
OTAPULSE_INSTALL_INPUT_PACKAGES, OTAPULSE_INSTALL_INPUT_PACKAGES_PATHy, input/packagesInstall the .deb files in that directory (the right architecture only). The conversion fails if soc-ota-agent or socmond is not installed afterwards, or if /etc/otapulse/boot_mode is not bootloader.
OTAPULSE_STORAGE_TOTAL_SIZE_MB8192Size of the medium the image is written to.
OTAPULSE_BOOT_PART_SIZE_MB, OTAPULSE_DATA_PART_SIZE_MB40, 128Boot and data partition sizes in MiB.
OTAPULSE_STORAGE_DEVICE_BASE/dev/mmcblk0pThe device file the partition numbers are appended to, in the bootloader, the fstab and the agent configuration. The Debian arm64 configuration sets /dev/vda.
OTAPULSE_PARTITION_SCHEMEthe scheme of the inputgpt or dos.
OTAPULSE_ENABLE_PARTUUIDnUse partition UUIDs instead of device names. Needs OTAPULSE_BOOT_PART, OTAPULSE_ROOTFS_PART_A, OTAPULSE_ROOTFS_PART_B and OTAPULSE_DATA_PART.
OTAPULSE_ARTIFACT_COMPRESSIONgziplzma, gzip or none. LZMA is 2 to 3 times smaller and about 10 times slower to write.
OTAPULSE_ASSETS_BASE_URLemptyWhere missing bootloader files (for the Raspberry Pi U-Boot) may be downloaded from. Every file is checked against configs/assets.sha256 either way. A missing or changed file stops the conversion.

socmond-watchdog reboots the device when the monitoring daemon dies. It also forces a reboot when free memory or free space on the root file system drops below 5 percent. That suits an appliance. On a general-purpose system it can reboot a machine that is only full. Converted images therefore ship it disabled. Set OTAPULSE_SOCMOND_WATCHDOG="y" to enable it.

The two packages are ordinary Debian packages. You can also install them on a system you prepared yourself.

PackageArchitectureContent
soc-ota-agentarm64, armhf, amd64The agent, the otapulse alias, soc-ota-provision, the systemd units, the boot guard, the update state scripts, /etc/otapulse/otapulse.conf and /etc/otapulse/boot_mode.
socmondallThe monitoring daemon (shell), its watchdog, the core dump handler, soc-ctl, soc-deregister, units and /etc/socmond/config.json.

Defaults you should know:

  • /etc/otapulse/otapulse.conf ships with BootEnvMode set to bootloader and UseFileBasedBootEnv set to false. The agent then reads the bootloader environment and never falls back to the file-based mode. If it finds no environment tool it stops with an error.
  • Artifact verification is required. The agent reads public keys from /etc/soc-monitoring/signing-keys/active/production-rsa-public.pem. Your image must put your key there.
  • /etc/otapulse/boot_mode holds bootloader. An absent file would mean native mode and would let the update scripts rewrite boot files that the bootloader owns.
  • The packages hold no credential.
  • /data must be a mount point. The data seed refuses to run onto the root file system, and the agent needs it. Without /data the agent does not start.
  • The data seed copies /etc/otapulse/device_type to /data/otapulse/device_type on the first start. If the data partition carries another device type, the unit fails and the agent does not start.
  • An upgrade of the soc-ota-agent package does not stop or restart the running agent. The new binary runs from the next start of the unit. An upgrade therefore cannot interrupt an install.
  • On purge, /etc/otapulse/device_type, /etc/socmond/provisioning_token and everything on /data stay.
  • The armhf package is built for ARMv7. It does not run on ARMv6 boards such as the Pi Zero or Pi 1.

socmond sets the core_pattern sysctl to collect crash dumps. A system with systemd-coredump installed will see that setting overridden.

A converted image does not include the first-boot registration service of the Yocto images. That service needs an administrator login in the image, which a customer image must not carry. So the agent and socmond register separately, and they show up as two server records for one device.

  • Agent: soc-ota-provision --server <url> --token <provisioning token> writes the per-device key to /data/otapulse/otapulse.conf (mode 0600) and restarts the agent. The data partition keeps it across updates.
  • Monitoring: put the token in /etc/socmond/provisioning_token (mode 0600) and set server.base_url in /etc/socmond/config.json. socmond provisions itself at its next start.

These commands registered a device on a plain Debian 12 arm64 VM. They have not been run on a converted image.

The tool prints no upstream product name in its command line, its messages, its option names or its file names. The debug log is the exception. It records every command, including the shell trace of the bootloader scripts. In that log, and in the message of a failed bootloader step, you can see file, directory and variable names of the bootloader integration. They are the real names of files on the image, so we do not rewrite them. See Names you may see in build logs and on devices.

  • Not OTA qualified. No update install, commit or bad-artifact revert through the agent has been run on a converted image. The slot switch and revert were a manual smoke test. We set the flags by hand, slot B booted with upgrade_available=1, we cut the power while it was uncommitted, and GRUB went back to slot A.
  • Slot B behavior of the agent was not observed. The machine id restore on a slot that an update has just written is not proven. Only a reboot on slot A was seen.
  • Registration. Not proven on a converted image. See above.
  • Kernel upgrades. An apt kernel upgrade reruns update-grub against the EFI partition that both slots share. We did not check that the generated GRUB configuration keeps the A/B scripts and panic=<n>.
  • The 20 minute boot deadline was not exercised on a converted image. See Boot deadline.
  • Raspberry Pi OS and U-Boot. Not run. The prebuilt U-Boot for the Raspberry Pi is pinned by its checksum only. Where to host it is open. Set OTAPULSE_ASSETS_BASE_URL to your own host.
  • The container is not reproducible. The base image is pinned by digest, but its apt packages are installed from the live repositories. Only the tool itself is reproducible. A snapshot mirror pin is a planned follow-up.
  • Console. The input image decides the serial console. Set OTAPULSE_GRUB_KERNEL_BOOT_ARGS.
  • Warning at boot. socmond.service: Unknown key 'StartLimitIntervalSec' in section [Service] is harmless on Debian 12.