kvm2: name the cached base image after the provider entry (H4)

baseVolumeName was base-<key>.<format>, byte-identical to what the kvm
provider builds in the same pool -- for URL images too, since kvm takes
hex(sum[:8]) and kvm2 takes hex(sum[:])[:16], the same eight bytes -- with
a completely separate lock table. The shipped config has a live kvm-home
entry and a commented-out kvm2-home entry on the same host, the same key
debian-13 and the same default pool, and the comment invites the operator
to enable it. Both integration tests use key test-image. On beelink both
live overlays back onto kvm's base-debian-13.qcow2.

Since cachedImage accepts any 64-hex marker when no sha256 is configured
and never re-hashes the base, that meant two entries pointing one key at
different URLs silently shared a file; a concurrent cold fetch let one
entry delete the other's half-uploaded base and boot a guest from the
remains; and a sha256 edit deleted the base in place under live overlays,
which libvirt permits without complaint.

The name now carries the provider entry: base-<entry>-<key>.<format>.
imageSource gained an Entry field and resolveImage takes the provider
name, so every production call site follows from imageSource.volumeName().
That closes the cross-entry sharing outright, kvm<->kvm2 and kvm2<->kvm2
alike, and turns the per-Provider imageLocks from a latent bug into a
correct one: with names unique per entry, two entries cannot contend for a
file, so no process-wide lock table is needed.

The entry name now reaches a file name, so New checks it against the
existing kvmIdentifier rule, exported as provider.IsKVMIdentifier rather
than duplicated. Provider names are otherwise validated for all types at
once and only for being non-empty and unique, so a name with a slash would
have produced a volume nothing could address.

One companion fix, because the naming alone does not stop it:
cachedImage's digest-mismatch branch deleted the marker and the base and
refetched. It now returns ErrPermanent naming both digests and telling the
operator what to do. That branch was the remaining path that unlinks a
good file under running domains, and the contract already said "add a new
key instead" -- the code was contradicting it. The no-marker and
non-hex-marker branches keep deleting, since there the base is debris.

Not fixed, and said so in the docs rather than left to look closed:
editing images.<key>.url is still a silent no-op, since nothing records
the URL; and cross-process fencing still needs the deferred atomic publish
(upload to a temp name, verify, publish), which lands where H2 put the
discard.

Migration is a non-event. The base-debian-13.qcow2 on beelink is kvm's,
kvm keeps using it untouched, and kvm2 downloads its own copy once per
host. Nothing is replaced under a running guest; the pool needs room for
both, which the prerequisites now say.

The eviction recipe in the guide named base-debian-13.qcow2, which after
this change is kvm's file -- following it would have deleted the wrong
provider's base. It now greps for the prefix and deletes the marker first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 17:57:02 +00:00
co-authored by Claude Opus 5
parent 9682737c69
commit c10f98a352
17 changed files with 258 additions and 70 deletions
+15 -7
View File
@@ -74,11 +74,18 @@ field and that catalogue edits affect only new VMs.
`sha256`, `format` qcow2 or raw, `firmware` uefi or bios) or a URL given
directly in `placement.image`. The provider downloads the image into the
host's storage pool on first use and caches it there as a base volume named
`base-<key>.<format>` (for a URL: `base-<first 16 hex of sha256(url)>.<format>`),
with a sidecar marker volume `base-<key>.<format>.sha256` holding the digest of
the verified upload, written last. A base volume without a marker is
`base-<entry>-<key>.<format>` (for a URL: `<key>` is the first 16 hex of
sha256(url)), where `<entry>` is the provider entry's name, with a sidecar
marker volume `base-<entry>-<key>.<format>.sha256` holding the digest of
the verified upload, written last. The entry is in the name because the `kvm`
provider builds `base-<key>.<format>` in the same pool from the same catalogue
shape, and two entries pointing one key at different URLs would otherwise share
a file; the cost is one copy per entry per host, and the entry's name is checked
against `kvmIdentifier` at construction. A base volume without a marker is
incomplete and is deleted and redone. When `sha256` is configured and differs
from the marker's content, the image is re-downloaded. The download streams
from the marker's content, the image is **not** replaced: that is `ErrPermanent`
naming both digests, because overlays back onto the base file and libvirt
unlinks it under running domains without complaint. The download streams
from the URL through the operator into `StorageVolUpload` with a running
sha256; nothing is buffered. `Content-Length` is required because the volume's
capacity must be declared before the upload; a response without it is
@@ -89,8 +96,9 @@ Proxy cannot cut it off. After a successful upload `StoragePoolRefresh` makes
the file usable as a backing image.
Catalogue edit warning for the guide: overlays back onto the base file, so
replacing or evicting a base volume while Proxies use it corrupts those VMs;
add a new key instead.
evicting a base volume while Proxies use it corrupts those VMs. A `sha256`
change is refused for that reason; a `url` change is a silent no-op, since the
cache is keyed by entry and key. Add a new key for either.
**Evidence:** kvm-provider.md "The image cache" and "Known gaps"; execution log
of 2026-09-04 (raw qcow2 upload plus pool refresh accepted by libvirt 11.6;
@@ -381,7 +389,7 @@ read `kvm2.hosts` and not `kvm.hosts`.
| `diskSizeGb` | | 10 | not negative | overlay capacity, quota precheck |
| `vendorData` | | | | seed `vendor-data` |
| `installGuestAgent` | | true | | seed `vendor-data` |
| `images.<key>` | | | key matches `kvmIdentifier` | volume name `base-<key>.<format>` |
| `images.<key>` | | | key matches `kvmIdentifier` | volume name `base-<entry>-<key>.<format>` |
| `images.<key>.url` | yes per entry | | http(s) URL | download |
| `images.<key>.sha256` | | | 64 hex | marker comparison |
| `images.<key>.format` | | qcow2 | qcow2 or raw | base volume XML, overlay backing format |