Volumes and providers¶
Volumes¶
Persistent volumes are identified by the orchestrator when they are
attached, for example through a device serial= field, not by scanning
filesystem labels across a fleet. First attachment formats and labels a
blank volume, seeding it from the image's own content at that path
unless the volume opts out (see
Volumes), and
later boots recognize the label. Because identity is owned by the
orchestrator rather than by a fleet-wide lookup, label collisions between
unrelated volumes are harmless.
The exact identifier mechanism is provider-specific. contemper assumes some stable identifier exists but does not prescribe one.
Planned design¶
Volumes are the next milestone. The decisions so far:
- Block devices everywhere. Every target attaches volumes as blank block devices, including Incus, which also offers filesystem volumes. Block devices are what every provider can offer, so one mechanism covers all of them.
- The guest prepares its volumes on every boot. Only a local hypervisor lets contemper create disks itself; real providers hand the VM a blank device. So volumes are always handled in the guest, never on the host, by one rule:
- an ext4 filesystem carrying the expected label is reused as-is and mounted, which is what lets a new image version run on the data of the previous one;
- a disk whose first and last MiB are all zeros is blank and gets formatted, then seeded from the image's own content at that mount point (if any), unless the volume opts out;
- anything else is left alone and logged as a mismatch, so the failure mode is an unmounted volume, never lost data.
- Declaration.
VOLUMEin the image, plus labelsio.contemper.volume.<path>.size, an optionalio.contemper.volume.<path>.name, an optionalio.contemper.volume.<path>.seed="false"to opt out of the seeding above, andio.contemper.root.sizefor the root disk. Command-line flags override them. A volume without a size is recorded without one, and deployment fails until a label or a flag provides it; contemper never guesses a size. - Names. A volume is identified by its path. Its name, at most 16
characters (the ext4 label limit), is derived from the path unless the
optional name label overrides it, which keeps a volume attached when
its path changes between image versions. Derivation: the
leading
/dropped and the remaining/turned into-, and, when that is too long, shortened with a hash of the path appended. Two volumes with the same name fail the conversion. The name is the filesystem label and, where the provider lets contemper choose, the disk serial. - fstab. contemper appends one line per volume,
LABEL=<name> <path> ext4 defaults,nofail 0 2, the same on every target. Images opt out withio.contemper.fstab="false", builds withconvert --no-fstab. - Guest metadata in
/etc/contemper/. contemper writes facts known at conversion time, never deployment-time data, which stays with the provider's own metadata channel.buildis written for every image:key=valuelines with the contemper version, target, architecture, source reference and digest, and support image digests, without a timestamp so the root filesystem stays reproducible. A local archive source is recorded by file name and digest only, so no paths from the build machine end up in the guest.volumesis written whenever volumes are declared, even with the fstab opt-out: one volume per line asname serial-pattern fs mountpoint, with the mount path last so it may contain spaces.volumes-noseedis written alongside it only when at least one declared volume opts out of seeding: one volume name per line; a missing file means every volume seeds normally. - A shell-script helper applies that rule. It is a POSIX
shscript, identical in every image, delivered as a published support image with one variant per init system (OpenRC and systemd) and merged only when an image declares volumes. An image with volumes but neither init system fails the conversion rather than silently getting unprepared volumes. A script rather than a binary keeps it architecture-independent and readable in the guest; the image must providemkfs.ext4. - Local instances.
deploy --to local-qemukeeps volume disks per instance, named after the image repository without its tag, so deploying a new tag of the same image reuses the volumes while the root disk starts fresh. - Manifest. Volumes in
contemper.jsonbecome objects (name, path, size, filesystem), with a new format version. The same change nests the resolved support-image variants inside thesupportobject.
Providers¶
Two providers come first:
- qemu is the reference case: it proves the disk boots with nothing
but a hypervisor, and doubles as the debugging baseline. Implemented
as
deploy --to local-qemu. - Incus is the real integration case, exercising agent injection, configuration and volumes. Planned.
The Incus provider must assume a remote Incus server. Local paths are not a safe assumption anywhere in the deploy path: the disk must be transferable, and after import everything refers to the image by fingerprint or alias rather than by file path.
Driving provider tools¶
Providers are implemented by running their own CLI tools as subprocesses, which keeps provider-specific knowledge in the tool that owns it. The conventions:
- Parse only structured output (
--format json,--output=json), never human-readable text. - Run tools with argument arrays, never shell strings.
- Check the tool is present, and its version, before doing any work.
- Pass the tool's error output through verbatim on failure.
- Decide partial-failure and rollback behavior explicitly, since subprocesses give no transactions.