# mvirt 0.14.11

migrations: no
wire: unchanged
rev: 7e4f1486a0f0a5ec0cf800b9356bc6bc43014135

0.14.11 — the four bytes that made every imported template unbootable

A VM created from a template on 0.14.10 does not boot, and its disk holds a qcow2 header where a
partition table belongs. The template importer tested for the wrong magic: `QCOW2_MAGIC` was
transcribed as `QFI\xff` when ADR-0080 moved the import out of the stor-agent into a Task pod, and
the real qcow2 magic is `QFI\xfb`. `is_qcow2()` therefore never matched, every qcow2 source was
shipped into the repository verbatim as if it were already raw, and hydration copied those bytes
onto LBA 0 of a boot disk. Nothing in the chain objected — the template went `Ready`, at the
qcow2's byte size rather than the image's.

It surfaced only now because a louder failure stood in front of it: until 0.14.6 the import died on
ENOSPC in the `/tmp` tmpfs. Once that was fixed the import started succeeding, and shipping qcow2
files as disks.

Auditing the rest of that chain found a second way the same importer produced a silently wrong
disk, and a third defect that would have destroyed the image during the repair.

- mvirt-import: the magic is `0xFB`. Format detection is `qemu-img info` now — the same decoder
  that performs the conversion, so it agrees with it by construction, and it covers vmdk/vdi/vhdx
  as well — with the corrected magic as the fallback. A ship-time guard refuses to put a file that
  still carries a container magic into the repository, so a detection miss becomes a failed import
  with a named reason instead of a VM that will not boot.
- mvirt-import: the download either delivers the whole body or fails. It had no completeness check,
  no retry, accepted an unsolicited `206 Partial Content`, and discarded the final write's error —
  and a small truncation passes `qemu-img convert` silently: cutting the Debian 13 image by 4 KiB
  still converts with exit 0 into a full-size raw, while a 1 MiB cut errors out. A lost final write
  is exactly that shape, so the import produced a full-size image with a damaged tail that then
  verified correctly at every later step. ADR-0080 had replaced `curl -fSL --retry 3` with none of
  its guarantees.
- cplane: `DELETE /v1/templates/{id}` asks the foreign key's question before it destroys anything.
  It emptied the template's repository scope first and only then attempted the row delete that
  `Volume.template`'s `on_delete = "restrict"` was always going to refuse — leaving a `Ready`
  template with no image, every VM cloned from it failing to start on a missing manifest, and the
  chunk-pool sweep reclaiming the blobs so it did not come back.
- The tests pin the four bytes against the qcow2 spec rather than against the constant; the old one
  wrote the constant's own bytes into its fixture and was green throughout the incident. The
  template-delete test now points the repository at a bucket that records what it is asked.
- images.toml: import:m4 (sha256:d45036ba…).

Upgrading does not repair an affected platform — the poisoned image is data, sitting in the
template's repository scope and in every disk cloned from it. Delete those disks, then the
template, then import it again; a new template id means a fresh scope, so the importer's
manifest-present idempotency skip does not hide the re-import. A correct Debian 13 import reports
`sizeBytes` 3221225472, not 343343104.

closes #211 #212


## Upgrading

Point your flake's `mvirt` input at this release and redeploy the control-plane
host first, then each node in turn:

```
nix run .#colmena -- apply boot --reboot --on <control-plane host>
nix run .#colmena -- apply boot --reboot --on <node>   # one at a time
```

This release adds no migrations, so the control-plane host rolls back the same way
a node does: boot the previous NixOS generation.
