TUTORIAL
Fifteen-minute trial
Download nothing exotic. Start a local Fleet reference, enroll one device, ship a good release, then ship a bad one and watch rollback.
You should not need to read RAUC, partition tables, or key ceremonies before the first commit and rollback. Those details are in DEVICE-INTEGRATION.md when you leave the simulator.
Requirements: Linux, Go 1.27.1 or a newer supported patch, Python 3. The QEMU profile also needs KVM or TCG and QUALIFY_QEMU_IMAGE pointing at a generic lab disk. Use $HOME/zyvor-qemu-lab/disk.img when that copy is present; otherwise build one with the recipe in QEMU-LAB.md.
One command¶
make build
./scripts/ota-demo up
That starts a local artifact server, zyvor-fleet-ref, and one enrolled simulator device. It then:
- Signs and uploads a healthy release and waits until the job is
committed. - Signs a release whose health file is missing and waits until the job is
rolled_back. - Starts a third release, stops the daemon during download, starts it again, and records either a resume through
committedorneeds_recoverywith the documented abort path.
The report is written to the demo work directory and printed at the end. It is the downloadable proof for this run. It is not Minewing evidence.
./scripts/ota-demo up --profile qemu # only if the generic lab disk already exists
./scripts/ota-demo down # stop a previous up left in the foreground with --serve
./scripts/ota-demo up --serve leaves Fleet and the device running so you can repeat the three examples under examples/scenarios/ yourself.
The seven steps, if you run them by hand¶
- Reference image. Simulator needs none. For RAUC, use
$HOME/zyvor-qemu-lab/disk.imgwhen that copy is present, or build with./scripts/hil/build-qemu-rauc-lab.sh /path/to/outand checksum it. Do not treat that disk as a Minewing image. - Fleet.
bin/zyvor-fleet-ref -token lab-secret -devices demo-1 -listen 127.0.0.1:8443with the TLS materialota-demogenerates, or use--serve. - Enroll. One device id and one token file.
ota-demowrites both. - Sign a release.
otactl keygenandotactl sign. The private key stays off the device. - Canary. Put the assignment on that one device. A multi-device wave lives in Zyvor Fleet, not in this agent.
- Watch.
otactl status jsonmoves through download, install, reboot, health, and commit. - Break it. Remove the health file (the failed-health scenario) and confirm
rolled_back.
Scenarios you can point at after --serve:
| Example | What you should see |
|---|---|
| examples/scenarios/healthy | committed |
| examples/scenarios/failed-health | rolled_back |
| examples/scenarios/interrupted | resume, or needs_recovery if you kill the daemon during install |
Command by command (simulator)¶
The rest of this page is the same lifecycle make demo runs, one command at a time. It never touches RAUC, D-Bus, or real disks.
Requirements match the section above.
1. Build and set up a workspace¶
make build
mkdir -p /tmp/zyvor-ota-tutorial/assets
cd /tmp/zyvor-ota-tutorial
OTA=/path/to/zyvor-ota/bin/otactl
OTAD=/path/to/zyvor-ota/bin/zyvor-otad
bin/zyvor-ota is the same bytes as bin/otactl. Commands below that say $OTA status and expect JSON should use $OTA status json. The bare status command is the colorful banner.
2. Generate a signing key and a device config¶
$OTA keygen ./keys
touch healthy # the health probe this config checks for
keygen refuses to run if ./keys already exists, and writes release.key
(0600, private) and release.pub (0644, public) — never commit release.key.
Embed the printed public key as the trust key, and point download_hosts and
the health check at files under this workspace:
PUBKEY=$(cat keys/release.pub)
cat > agent.json <<JSON
{
"device_id": "tutorial-1",
"compatible": "tutorial-board",
"state_dir": "/tmp/zyvor-ota-tutorial/state",
"socket": "/tmp/zyvor-ota-tutorial/agent.sock",
"backend": "simulator",
"allow_device_writes": false,
"trust_keys": {"tutorial": "${PUBKEY}"},
"download_hosts": ["127.0.0.1:8091"],
"max_artifact_bytes": 1048576,
"reserve_bytes": 1048576,
"health_timeout_seconds": 10,
"health_stable_seconds": 2,
"checks": [{"kind": "file", "target": "/tmp/zyvor-ota-tutorial/healthy"}]
}
JSON
Every field here is documented in the User Guide.
3. Start an artifact server and the daemon¶
python3 -m http.server 8091 --directory assets --bind 127.0.0.1 &
$OTAD -config agent.json &
sleep 1
$OTA -socket ./agent.sock status
{"active":null,"backend":"simulator","device_id":"tutorial-1","event_sequence":0,"high_sequence":0,"pending_events":0,"version":"0.2.0"}
4. Build and sign a release¶
echo "tutorial release payload" > assets/os.raucb
SHA=$(sha256sum assets/os.raucb | awk '{print $1}')
SIZE=$(wc -c < assets/os.raucb | tr -d ' ')
cat > release.json <<JSON
{
"schema": 1, "id": "tutorial-release-1", "sequence": 1,
"compatible": "tutorial-board", "backend": "simulator", "version": "1.0.0",
"expires": "2030-01-01T00:00:00Z",
"artifact": {"url": "http://127.0.0.1:8091/os.raucb", "sha256": "${SHA}", "size": ${SIZE}}
}
JSON
$OTA sign release.json keys/release.key tutorial envelope.json
sequence is the anti-replay high-water mark: it must strictly increase
across releases this device will accept. sign writes envelope.json — the
key_id, base64 payload, and Ed25519 signature that submit verifies.
5. Submit a signed assignment¶
Assignments carry their own install window (not_before/deadline),
independent of the release's own expires:
NOT_BEFORE=$(date -u -d '-1 minute' +%Y-%m-%dT%H:%M:%SZ) # macOS: date -u -v-1M ...
DEADLINE=$(date -u -d '+10 minutes' +%Y-%m-%dT%H:%M:%SZ) # macOS: date -u -v+10M ...
python3 -c "
import json
envelope = json.load(open('envelope.json'))
json.dump({'job_id': 'tutorial-job-1', 'device_id': 'tutorial-1', 'release': envelope,
'not_before': '$NOT_BEFORE', 'deadline': '$DEADLINE', 'auto_reboot': False},
open('assignment.json', 'w'))
"
$OTA -socket ./agent.sock submit assignment.json
The response is the created job at state: "accepted". Poll it — the engine
advances once per second:
$OTA -socket ./agent.sock job tutorial-job-1
Within a few seconds it moves accepted → downloading → verified →
installing → awaiting_reboot on its own (downloading the artifact, verifying
its digest, and writing it to the inactive slot). At awaiting_reboot:
{"...":"...","state":"awaiting_reboot","old_slot":"rootfs.0","old_version":"factory","target_slot":"rootfs.1","boot_id":"boot-0","reboot_requested":false,"updated":"..."}
6. Reboot and watch it commit¶
auto_reboot was false, so nothing reboots until you ask:
$OTA -socket ./agent.sock reboot
Poll job tutorial-job-1 again. It moves checking_health → committing →
committed once the configured health check (the healthy file) has passed
continuously for health_stable_seconds:
{"...":"...","state":"committed","old_slot":"rootfs.0","target_slot":"rootfs.1","boot_id":"boot-0x","reboot_requested":true,"updated":"..."}
7. See a rollback happen¶
Submit a second, higher-sequence release the same way (sequence: 2,
job_id: tutorial-job-2), wait for awaiting_reboot, then remove the health
marker before rebooting so the new slot never becomes healthy:
rm -f healthy
$OTA -socket ./agent.sock reboot
After health_timeout_seconds (10s here) with no passing health check, the
job moves checking_health → rollback_pending → rolled_back — the backend
restores the old slot automatically, no operator action needed:
{"...":"state":"rolled_back","error":"health verification deadline exceeded","old_slot":"rootfs.1","old_version":"1.0.0","target_slot":"rootfs.0","boot_id":"boot-0xx","...":"..."}
Resubmitting either assignment.json again is rejected — its release
sequence is no longer above the device's high-water mark. This is the same
check that rejects a captured-and-replayed assignment.
8. Acknowledge events and clean the cache¶
$OTA -socket ./agent.sock events # every state transition since the last ack
$OTA -socket ./agent.sock ack 18 # the last sequence number from `events`
$OTA -socket ./agent.sock events # -> null: nothing unacknowledged left
$OTA -socket ./agent.sock gc # removes cached bundles now that no job is active
A real Fleet server does the ack step after it durably stores the events
(see FLEET); this tutorial does it by hand since there's no Fleet
server here.
9. Clean up¶
kill %1 %2 # the artifact server and the daemon started with `&` above
rm -rf /tmp/zyvor-ota-tutorial
Next steps¶
- User Guide — every CLI subcommand and config field in one place.
- Device deployment — the same lifecycle against a real board with the RAUC backend.
- Remote smoke-deploy — run this same simulator lifecycle as an installed systemd service on another host over SSH.
- Operations — the runbook for
NeedsRecovery, trust rotation, and journal limits once this isn't a toy workspace anymore.