Appearance
Flashable device images
The builder makes the image a device is flashed with, such as the eMMC image of a Raspberry Pi CM5. The image comes signed for one of your signing identities and carries your apps and the fleet's configuration. It takes two kinds of build:
- An image base, once per machine and meta-pantavisor version. This is a Yocto (kas) build of meta-pantavisor's
pantavisor-image-base, which holds the BSP, the core containers, the boot selector and the partition layout. It knows nothing of your identity. - Images, as often as your apps or configuration change. An image build customises a base in minutes, with no Yocto:
- your identity's trust and
PV_OEM_NAMEgo into the initrd; - its platform key signs the BSP again;
- your apps and an OEM configuration part join the factory revision;
- it writes one image per policy.
- your identity's trust and
Only the Raspberry Pi tryboot A/B layout (Pi 3, 4 and 5, CM4, CM5) has an image base for now.
1. Build the image base
Start a build of https://github.com/pantavisor/meta-pantavisor, at the branch or tag you ship, with What to build: Image base. In the web UI the kas files are filled in for the Raspberry Pi:
kas/machines/rpi.yaml
kas/scarthgap.yaml
kas/bsp-base.yaml
kas/build-configs/build-image-base.yamlThe build starts from meta-pantavisor CI's shared cache, built with the same pvgo layer (Go 1.24), so it is usually quick, though a build may run up to four hours. It makes pantavisor-image-base-raspberrypi.tar. The architecture you pick only labels the build.
With pvr:
sh
pvr build start --project cm5 --target image-base \
--git-url https://github.com/pantavisor/meta-pantavisor --git-ref master \
--image-base kas/machines/rpi.yaml,kas/scarthgap.yaml,kas/bsp-base.yaml,kas/build-configs/build-image-base.yaml2. Build the images
You need:
- a signing identity with a platform key. Its hosts must cover the base's repository: the default
github.comdoes; - app builds signed with the identity's OEM key (
<identity>-oem). The image build refuses apps a device would refuse; - optionally, an OEM configuration part with one config per policy, such as
greenandred, each naming the config secret of its device join token (PH_FACTORY_AUTOTOK).
Choose Source: Flashable device images, then:
| Field | What it sets |
|---|---|
| Start from | an image base, or a factory build (below); a factory fills in its identity, policies and secure boot mode |
| Signing identity | the trust and PV_OEM_NAME in the initrd; only identities with a platform key can be chosen |
| App builds in the images | succeeded app builds; each must be signed with the identity's OEM key |
| Device and Revision (optional) | the apps of one of your devices' revisions, such as a golden device; their signatures must chain to the identity too |
| Policies | PV_POLICY, one image each (up to 4); empty makes one image without a policy |
| Secure boot | PV_SECUREBOOT_MODE: strict (default), lenient, audit or disabled |
| Pantahub host (optional) | creds.host of the image's pantahub.config; by default the PH_CREDS_HOST every policy's OEM config sets, else the base's (api.pantahub.com). See which Hub the devices join |
Add the OEM part under App name and secrets → Add an OEM configuration part, as for an app build.
With pvr (--base makes it an image build):
sh
pvr build start --project cm5 --target image --base <image base build id> --identity acme \
--app-build <app build id> --device golden@12 --policy green --policy red --oem oem.json
pvr build download --name acme-green.img.xz <image build id>Container group timeouts
Pantavisor starts containers by group (data, root, platform, app), each with a timeout in the BSP's device.json, 30 seconds by default. During an update, a group that has not reached its containers' status goal in time fails the update, and the device rolls back. A slow machine, or containers that wait for the network, can need more. Set it in the build with --group-timeout NAME=SECONDS (1 to 86400, repeatable):
sh
pvr build start --project cm5 --base <image base build id> --identity acme \
--policy green --policy red --make-factory \
--group-timeout root=120 --group-timeout platform=120device.json is part of the BSP, signed with the platform key, so the timeouts are set where the BSP is signed: in a factory build (or an image build from a base), never in an image built from a factory, which takes the factory's. A group the base does not have fails the build. The web UI and the MCP connector do not set it yet; the API takes it as device_image.group_timeouts. Locally, pvr image factory and pvr image assemble take the same flag.
Each policy gives two artifacts:
| Artifact | What it is |
|---|---|
<identity>-<policy>.img.xz | The image to flash. |
<identity>-<policy>.pvrexport.tgz | Its factory revision, for updates that keep this image's BSP. |
Before writing an image the build checks every signature against the identity's trust, as a device does, and in strict mode that every key is signed. An image a device would refuse fails the build instead.
Factory images: build once, add apps in seconds
Most of an image build depends only on the base, the identity and the policy: the initrd's trust and PV_POLICY, and the BSP and core containers signed with the platform key. A factory build does that part once:
- choose Start from an image base, and tick Make factory images, with the policies (green, red);
- with pvr:
pvr build start --base <image base build> --identity acme --policy green --policy red --make-factory.
It keeps one <identity>-<policy>.factory.tar per policy. A factory image holds no secret, only the identity's public trust, so it is kept and reused. Images then start from it, with the apps and the OEM part: assembly takes seconds, and no platform key signs anything but the OEM part.
sh
pvr build start --project cm5 --target image --factory <factory build id> --identity acme \
--app-build <app build id> --oem oem.jsonLocally: pvr image factory --base … --trust … --policy green --key … --x5c … -o acme-green.factory.tar, then pvr image assemble --base acme-green.factory.tar --export … -o acme-green.img.xz (no key needed).
Secrets after a build
- Keys, config secret values and download links reach only the runner that builds, are never stored with the build nor shown in its log, and its workspace is deleted when the build ends; a runner that restarts deletes what a lost build left.
- Images and exports whose OEM part names config secrets hold the device join tokens. They are marked Holds device join tokens and deleted 7 days after the build (the build then shows them Expired). Download and flash them before; build again for more.
- Image bases and factory images hold no secret and are kept.
Flash it
Download the image from the build's Artifacts tab, or ask the MCP connector for get_build_artifact_link. The file goes straight to disk: check it against the sha256 shown.
sh
sha256sum acme-green.img.xz
# CM5 eMMC: put the module in USB boot mode, then
sudo rpiboot && xzcat acme-green.img.xz | sudo dd of=/dev/sdX bs=4M conv=fsync status=progressRaspberry Pi Imager and bmaptool take the .img.xz too.
On first boot the device:
- checks its factory revision (revision 0): every signature, as the build did, and that each document of the state is in place;
- grows its storage;
- starts its containers group by group; the factory revision's containers are all in it, linked from
trails/0; - loads
<identity>/<policy>.config; - joins the Hub in
PH_CREDS_HOST(or the image'screds.host) with that policy's token, and appears in the devices of the account that owns the token, with the token's default user-meta.
The device needs a network to join: Ethernet, or Wi-Fi set up by an app such as pvwificonnect. With pvwificonnect, a device with no network advertises itself over Bluetooth LE, and pvr gives it one from a laptop nearby:
sh
pvr wifi device-scan # lists devices in range: address, name, state
pvr wifi provision <address> --ssid <network> --password '<passphrase>'
pvr wifi device-info <address> # firmware, hardware, nameA reflash wipes the stored network, so provision it again after flashing.
Which Hub the devices join
Two settings, the second wins:
- Pantahub host of the image build (
--pantahub-host): written into the image'spantahub.config. PH_CREDS_HOSTin the policy's OEM config: read from the signed OEM part at every boot.
So one image base, and one factory, serve every Hub: to test against stage, put "PH_CREDS_HOST": "api.stage.pantahub.com" in the policy's config, with a token made on stage.
Both should name the same Hub. Pantavisor writes the Hub it reports (/run/pantavisor/pv/pantahub-host, which pv-avahi advertises) from pantahub.config before it loads the OEM config, so an image whose OEM config moves it to stage would still advertise api.pantahub.com. An image build therefore takes its Pantahub host (and PH_CREDS_PORT as its port) from the OEM part when every policy sets the same literal value and none was given; a given host that differs fails the build. A value from a config secret is not known to the build and is left out.
When a device does not come up
Watch its serial console (pvr device tty -d /dev/ttyUSB0 log), or, once it has joined, pvr device logs and pvr device ssh.
| On the console | Cause |
|---|---|
bad factory revision, could not load /storage/trails/0/<file>.json, or containers that do not start from revision 0, then a reboot | The image was assembled by a pvr older than the fixes that write the revision's documents in trails/0 and link its objects there. Build the image again; the factory image need not change. |
An update rolls back with a group that timed out before all goals were met | A container group took longer than its timeout (30 s by default) to reach its status goal. Make a factory with a longer group timeout and flash an image of it. |
It joined stage, but pv-avahi and pvcontrol report api.pantahub.com | The image's pantahub.config names another Hub than its OEM config. Build it again: the build now takes the host from the OEM part. |
A signature refused, or strict secure boot refuses | A part is not signed for this identity; the build should have failed, so check the image came from this build. |
| Boots, but never appears on the Hub | No network; a token of another account or Hub (check PH_CREDS_HOST); or a token that was deleted. |
Example: a CM5 on stage with the MQTT agent
The Pantahub MQTT agent (pv-mqtt-sdk) replaces Pantavisor's own Hub client: updates arrive over MQTT as soon as they are posted, with device-meta, user-meta and web SSH on the same connection. It runs only on a device whose configuration turns Pantavisor's client off, which for a signed image means its OEM part. The agent is not open source: Pantacor ships it as prebuilt pvr exports per device on pantavisor.io/downloads.
Image. Build a factory with longer group timeouts (above), then an image from it with your app builds and an OEM part whose configs join stage with remote control on, so the device can join and take its first update:
json{ "name": "acme", "signing_secret": "acme-platform", "configs": { "green": { "PH_CREDS_HOST": "api.stage.pantahub.com", "PH_FACTORY_AUTOTOK": {"secret": "acme-green-autotok"} } } }Flash it, give it Wi-Fi with
pvr wifi provision, and it appears in the token's account.The agent. Download its export for your device from pantavisor.io/downloads. It needs an OEM part with remote control and log push off; the device keeps its join and its credentials, so the token is no longer needed. Build that part on its own with an app build that carries
--oem agent-oem.json(signed with the identity's platform key):json{ "name": "acme", "signing_secret": "acme-platform", "configs": { "green": { "PH_CREDS_HOST": "api.stage.pantahub.com", "PV_CONTROL_REMOTE": "0", "PV_CONTROL_REMOTE_ALWAYS": "0", "PV_LOG_PUSH": "0", "PH_UPDATER_NETWORK_TIMEOUT": "600" } } }The agent's export must be signed with a key a
strictdevice trusts. Drop an unsignedpv-oem-developer-001/default.configpart if the export carries one: astrictdevice refuses it.The update. Merge the export into the device's next revision without that part. With the MCP connector:
get_export_upload_link(orimport_export_from_url) for the agent's export andimport_build_artifactfor the OEM part, thenplan_revisionwithimport_exportfor each andremove_parts: ["pv-oem-developer-001"], thencommit_revision. With pvr:pvr merge,rm -r pv-oem-developer-001,pvr add ., commit and post. Pantavisor's client installs this one; from the next boot the agent runs instead.Check it.
pvcontrol lsshowspv-mqtt-sdkSTARTED, and its log (/storage/logs/<rev>/pv-mqtt-sdk/lxc/console.log) shows it logging in and connecting:agent: logged in (owner=prn:::accounts:/…) agent: message plane up, subscribed to 3 topics agent: relayed DONE for rev 1, installed by pantavisor before the agent took overPost one more revision, any change. The Hub pushes it to the agent at once; the agent downloads it, hands it to Pantavisor as a local revision (
locals/hub-<rev>) and relays every step back, so the revision walks to DONE on the Hub as usual:agent: PUSH step notification: {"rev":2,…,"status":"NEW",…} agent: applying rev 2 agent: rev 2 handed to pantavisor as locals/hub-2 agent: relayed INPROGRESS 50% (msg: Transitioning to new revision without rebooting) for rev 2 from pantavisor
Green, red and updates
PV_POLICY is an image-level setting, read from the initrd, and the initrd is part of the BSP. So green and red devices run different BSPs. Update each group from its own policy's factory export, or with app-only revisions. Never move a device to the other policy's BSP: it would change policy on the next boot.
Keys that never leave your company
The assembly is pvr's, so it runs where your keys are. It needs mtools, e2fsprogs and xz, and no root.
sh
pvr signing trust acme -o trust # the identity's public trust
pvr image assemble --base pantavisor-image-base-raspberrypi.tar \
--trust trust --policy green \
--key platform-key.pem --x5c platform-chain.pem \
--export hello.pvrexport.tgz --export oem.pvrexport.tgz \
-o acme-green.img.xz --export-out acme-green.pvrexport.tgzBoot partitions
The Raspberry Pi bootloader does not check what it boots. Pantavisor verifies the revisions it runs, but someone with the device in hand can change its boot partition. To prevent that, enable Raspberry Pi secure boot (signed boot images and OTP keys) on top.