-
Notifications
You must be signed in to change notification settings - Fork 0
Tutorial 5 Distribution
← 4. Write a rig (rig.py) · Tutorial index · 6. Provision a board + deploy from the Ground Station →
You now have an app (.art + C++) and a rig (manifest/single/rig.py). This
chapter closes the local dev loop — theia install → theia start → theia stop → theia clean — and then maps those artifacts onto the distribution model
that the fleet uses for OTA.
Four commands form the local inner loop:
| command | what it does |
|---|---|
theia install <target> |
serialize the rig → per-machine JSON; bazel-build; stage install/<machine>/
|
theia start |
launch the staged supervisor (daemonized, pidfile) |
theia stop |
SIGTERM the supervisor → waits for clean shutdown |
theia clean |
remove install/ + dist/manifest/; --bazel also runs bazel clean
|
These are symmetric: install and clean are inverses; start and stop are
inverses. Run them from your workspace root.
theia install single # build + stage install/central/
theia start # supervisor up, log → install/central/supervisor.log
# … work, probe, restart …
theia stop # graceful shutdown
theia clean # wipe install/ + dist/manifest/
theia clean --bazel # also bazel clean (full rebuild next time)theia install single runs three steps in sequence:
Step 1 — serialize the rig.
artheia serialize-manifest manifest.single.rig writes the per-machine JSON
manifests under install/manifest/central/:
install/manifest/
machines.json { "machines": ["central"], "rig": "single", … }
central/
machine.json arch, os, cores, etcd, machine_states
execution.json process list (names → bazel targets + start_cmd)
executor.json the full supervised process tree (for the supervisor)
service.json, application.json
config/
executor.json collocated copy the supervisor reads at runtime
<fc>.json per-FC params defaults (from params{} in .art)
config-defaults.json first-boot etcd seed (from config{} in .art)
The config/ sub-tree is key: params and config defaults are captured at
gen-manifest time and baked into the manifest module (PROCESS_PARAMS /
PROCESS_CONFIG_DEFAULTS sidecars). serialize-manifest emits them directly — no
backtrack into .art at install time.
Step 2 — bazel build.
Reads execution.json for the binary set; builds only what's needed. Framework
binaries (supervisor) come from the deb install (/opt/theia/bin) in deb-mode, so
only your own //apps/... are compiled.
Step 3 — stage.
Copies config/ verbatim from the manifest, stages the supervisor and FC
executables under install/central/, and applies setcap (same caps a real Ansible
deploy uses).
The result:
install/central/
supervisor the supervisor binary (setcap'd)
current/ → symlink to releases/local/
releases/local/
bin/
p1 your FC executable
config/
executor.json the supervisor tree (read via THEIA_SUPERVISOR_MANIFEST)
p1.json params defaults for p1's nodes
config-defaults.json first-boot etcd seed
theia start launches the staged supervisor as a background process:
theia start # → "supervisor up (pid …) — log: install/central/supervisor.log"The supervisor:
- reads
config/executor.jsonfor the process tree, - starts each child with
THEIA_CONFIG_DIR=configso the runtime config singleton findsconfig/<fc>.json, - writes a pidfile at
install/central/supervisor.pid.
Check the log and the process tree:
tail -f install/central/supervisor.log
tdb apps # the supervised tree (requires a running supervisor)
tdb ps # flat Linux-ps viewTwo kinds of per-FC data land in config/:
Params (config/<fc>.json) — static deploy knobs, read once at boot by the
runtime config singleton. They come from params {} blocks in your .art:
For the tsync service, whose nodes have GPS/PTP knobs:
// config/tsync.json
{ "nodes": { "tsync_ctl": { "gps_baud": 460800, "poll_ms": 100, … }, … } }Config defaults (config-defaults.json) — the first-boot etcd seed for
config {} (runtime-observable protobuf config). Seeded on theia start via
gen-config-defaults:
// config-defaults.json
{ "configs": { "counter": { "digest": "cfg_…", "values": { "step": 1, "label": "counter" } } } }After seeding, per stores these in etcd. You can inspect or change them live via
tdb set-config without restarting.
Per-machine overrides. For a split rig where the same FC runs on both machines with different knobs (e.g.
tsyncas GPS-master on central, GPS-slave on compute), place a hand-edited JSON indeploy/config/<machine>/<fc>.json.serialize-manifestdeep-merges it on top of the.artdefaults at manifest time — no code changes needed.
The local loop is the dev path. For the fleet, the chain is
manifest → dist → release*, with one build verb (dist) that behaves
differently for a runtime manifest vs an app manifest:
| verb | input | output | deploy path |
|---|---|---|---|
theia manifest <target> |
rig.py |
dist/manifest/ — per-machine JSON |
prerequisite for everything below |
theia dist <target> |
dist/manifest/ |
runtime manifest → dist/debian/theia-{runtime,services}_*.deb; app manifest → dist/manifest/<host>/<host>.deb
|
(build only) |
theia release <target> --s3 URL |
dist/manifest/ |
runtime .debs → S3 theia-runtime/<ver>-<abi>/
|
colony installs the runtime plane |
theia release-swp <app> |
dist/manifest/ + bazel |
dist/apps/<app>/<rig>-<ver>-<abi>.mender |
Mender OTA installs as overlay |
One build verb, two modes. theia dist <target> keys on the manifest's
machines.json apps list. A runtime manifest (apps: [] — e.g. the framework
services rig) is associated with the runtime Deb set and builds
theia-runtime + theia-services; an app manifest (apps: [...] — your single
rig) has no association and packs one self-contained .deb per machine. --arch A[,A…] overrides every machine's arch, so one runtime manifest releases for several
arch/os in a single call. (This is the build that the old build-only misnomer
theia release --arch used to do.)
Two deploy paths, still independent:
-
Ansible path (app
.deb→theia orchestratein thecolonyrepo): the self-contained per-machine.debfromtheia dist <app-target>, SSH-copied anddpkg -i'd. Direct provisioning of known boards — no S3 needed. -
Mender/OTA path (
theia release-swp→ S3 → Mender): a.menderoverlay of only your app FCs, installed on top of the running runtime (supervisor + services untouched). Day-2 updates and fleet OTA.
The Mender path requires a running runtime base provisioned by colony (which installs
the runtime plane theia release <services-target> --s3 publishes); the Ansible path
is self-contained.
The runtime plane (supervisor + ARA services) is built once per runtime version —
the platform team's responsibility, not the app developer's day-to-day. It comes
from the framework's role-based services manifest (apps: []), so theia dist
associates it with the runtime Deb set:
# from the theia framework checkout:
theia manifest services # the role-based runtime manifest (central/zonal)
theia dist services --arch host # → dist/debian/theia-{runtime,services}_*amd64.deb
# or: theia dist services --arch rpi4 # bookworm-arm64
# or: theia dist services --arch jetson # focal-arm64Output: dist/debian/theia-runtime/, dist/debian/theia-services/. Publish them to
the S3 runtime plane with theia release (the push counterpart to theia dist):
theia release services --arch host --version 0.2.2 \
--s3 http://<minio-host>:9000
# → s3://theia-runtime/0.2.2-amd64/theia-{runtime,services}_*.debtheia release <target> --s3 builds via theia dist <target> then aws s3 cps the
runtime .debs to theia-runtime/<ver>-<abi>/. The runtime_build key in a
Distribution ("0.2.2-amd64") references this versioned upload verbatim; colony reads
it at provisioning time to know which .debs to install on a fresh board.
Two verbs, symmetric with the app plane:
theia release <services-target>pushes the runtime plane (colony installs it);theia release-swp <app>pushes the app plane (Mender overlays it). Same shape, different bucket + consumer.
The Ansible path takes your app from a rig all the way to a .deb that Ansible
SSH-copies and dpkg -i's:
theia manifest single # → dist/manifest/ (the per-machine JSON)
theia dist single # → dist/manifest/central/central.debtheia dist <target> reads dist/manifest/machines.json for the machine list (the
<target> name is required — it validates the manifest is for the right rig), looks
up each machine's arch from machine.json, and builds //dist/manifest:<host>_pkg
(a cross-compiled .deb per host). The .deb carries only your app FCs — the
supervisor and ARA services are part of the runtime base installed by colony:
$ dpkg-deb -c dist/manifest/central/central.deb
./opt/theia/bin/p1The .deb is staged next to the machine's manifest JSON — exactly where
orchestrate.yml expects it.
# then deploy to a known board (from the `colony` repo — the deploy adapter):
colony orchestrate # Ansible: copies + dpkg -i, sets up systemd unitProvision/orchestrate moved to the
colonyrepo (the deploy adapter).theiaemits the per-machine bundle viamanifest/dist;colonydeploys it.
This path does not touch Mender. It is the right choice for a controlled lab board that you can SSH into directly.
The Mender/OTA path builds a .mender artifact carrying only your app FCs —
the supervisor and services are never touched. It reads dist/manifest/ (from
theia manifest) for the process set, builds every composition's executable, and
packs them:
theia manifest single # prerequisite: emits dist/manifest/
theia release-swp apps --swp-version 1.0 # build-only, no S3
# → dist/apps/apps/single-1.0-amd64.mender
theia release-swp apps --swp-version 1.0 \ # build + publish to S3
--fleet theia-rig \
--s3 http://<minio-host>:9000
# → s3://theia-swp/user-software/theia-rig/single/1.0-amd64/| flag | meaning |
|---|---|
apps |
the apps/<app> package (your cluster's home in //apps/apps/) |
--swp-version |
your app semver — the install dir + artifact name |
--fleet |
the Mender device-type / hardware-capability class (S3 key) |
--arch |
board target host/rpi4/jetson — picks abi; default host
|
--s3 |
MinIO/S3 endpoint; omit to build the .mender locally only |
The artifact is named <rig>-<ver>-<abi>.mender (rig name from machines.json,
not the app package name) — so single-1.0-amd64.mender for the single rig. This
is the SWP build identifier (swp_build) the Distribution references.
Key points:
-
No
.debstep. The SWP is a Mender overlay (theia-swpmodule type), not a.deb. The supervisor + services.debs are the runtime base, installed by colony at provisioning time — the SWP overlays only your FCs on top. -
Multi-composition.
release-swppacks all compositions in the cluster into one overlay. Theappscluster here has one composition (CounterProc→p1); the demo's cluster bundles four. -
arity/roles/onfrom the manifest.release-swpreadsdist/manifest/machines.jsonfor these — no hand-written metadata. -
Per abi for heterogeneous fleets. An rpi4(bookworm)+jetson(focal) split rig
needs two builds:
--arch rpi4and--arch jetson.
A Distribution is the deployable bundle: a name + version + a list of roles,
each resolved to {abi, runtime_build, swp_build}. The invariant: role.abi == runtime_build's abi == swp_build's abi. arity = len(roles).
The operator creates one in the Ground Station UI (Distributions view: pick the app → per-role pick abi + runtime build + SWP build). Behind the UI it is a single API call:
// POST /api/planes/distributions
{
"name": "single", "version": "1.0",
"roles": [
{ "role": "central", "abi": "amd64",
"runtime_build": "0.2.2-amd64", "swp_build": "single-1.0-amd64" }
]
}A split (arity-2) Distribution lists two roles:
{
"name": "split", "version": "2.0",
"roles": [
{ "role": "central", "abi": "amd64",
"runtime_build": "0.2.2-amd64", "swp_build": "split-2.0-amd64" },
{ "role": "compute", "abi": "amd64",
"runtime_build": "0.2.2-amd64", "swp_build": "split-2.0-amd64" }
]
}Stored at s3://theia-distributions/<name>/<version>/index.json. The Ground Station
is stateless — every entity is an S3 artifact.
One Distribution deploys a whole heterogeneous fleet in one click: it resolves each role to a board whose abi matches, then fans out per role — the runtime build to colony (the base), the SWP build to Mender (the app overlay). Runtime↔app compatibility is satisfied by construction (you bound them in the Distribution), so there is no deploy-time guesswork.
Distribution "split" 2.0
├─ role central (amd64) → runtime 0.2.2-amd64 + swp split-2.0-amd64
└─ role compute (amd64) → runtime 0.2.2-amd64 + swp split-2.0-amd64
↓ deploy
for EACH role: colony(base) + Mender(app) to its assigned board
You now have: the local install loop (install / start / stop / clean),
a clear picture of the four release verbs and two deploy paths (Ansible vs Mender),
and the Distribution model that ties runtime + SWP together for fleet OTA.
Next: Chapter 6 — Deploy from the Ground Station, where you bring up a rig, enrol it through the GS, and deploy your Distribution onto it.
← 4. Write a rig (rig.py) · Tutorial index · 6. Provision a board + deploy from the Ground Station →