Skip to content

One definition of the machine, imported instead of spelled again - #81

Merged
aledbf merged 4 commits into
mainfrom
feat/one-machine-definition
Sep 8, 2026
Merged

One definition of the machine, imported instead of spelled again#81
aledbf merged 4 commits into
mainfrom
feat/one-machine-definition

Conversation

@aledbf

@aledbf aledbf commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

spinbox kept its own copy of what a VM is: a command-line builder (402 lines), a kernel command line (257), and a MachineIdentity beside them that spelled the same four arguments a second time so a template's fingerprint could be computed before there was a command line to read. The two were kept in step by a shared machineShape and a comment asking the next person not to break it — because nothing checks a restore at run time. A template loaded into a machine of another shape is memory and device state going into hardware it did not come from, and it does not fail, it misbehaves.

There is one machine.Spec now. The command line is Args() of it and the fingerprint is Fingerprint() of it, so the two cannot disagree because there is nothing left to disagree.

Net −2,361 lines.

The two definitions had already drifted, in both directions

  • It hashed runtime.GOARCH, which cannot differ without the QEMU binary differing.
  • It hashed the host CPU unconditionally — right under -cpu host, wrong the moment a model is named, where it would partition templates per machine exactly when the point of naming one is that they cross machines.
  • It did not hash the device list at all, so adding the balloon left every existing template matching a machine it could no longer be restored into.

Memory grows through virtio-mem

The -m line has no slots= any more, so the pc-dimm path went with them: a backend object and a device per step, a table of which of the eight slots were in use, LIFO ordering so unplug took the newest, an RPC into the guest to online what arrived and another to offline what was leaving, and a rollback for each of the four ways that could half-fail. One qom-set replaces all of it — the guest onlines by itself (memhp_default_state=online) and shrinking is the same call with a smaller number.

What the device reports is read back rather than assumed. A request is a negotiation: virtio-mem plugs as the guest accepts and unplugs only what the guest has released, so recording the number we asked for would leave the controller believing in memory the VM does not have, and every threshold after that computed against it. There is a test for exactly that.

Every disk is a qcow2

The format was worked out from the file extension while the command line was being built — a guess in the one place nobody looks, and a wrong one is a guest that boots and finds a disk full of nothing. There is one format because there is one thing that produces them: a chain with a read-only base many VMs map at once and a read-write tip that is this VM's.

One layout

The release tree is the layout everywhere, so hack/spin-machine copies instead of rearranging, and internal/paths stops guessing — no candidate lists ending in /usr/bin, no second kernel name, no discoverQemuPath. A host with no release used to run a QEMU, with different devices and a different fingerprint, and find out by way of a guest that would not start. Config validation now opens the release, so a host missing one of the four files is told which one, once.

Deleted rather than adapted

qemu_command_test.go and the fingerprint/machineShape tests: they test machine.Spec upstream now, against more properties than these did. qmp_memory_test.go: every test in it was either a 128 MB alignment rule that no longer exists or a t.Skip("manual integration test") that has never run.

Verified

task lint (0 issues), go test -race ./..., and hack/spin-machine against a sibling checkout.

Before merging

go.mod pins spin-machine at a branch commit — this needs spin-stack/spin-machine#1, and the pin must be moved to a tagged release once that merges. This invalidates every template on every host, which is what changing the machine means; nothing in production consumes one yet.

The System service still carries OnlineMemory/OfflineMemory and the guest still implements them. Nothing on the host calls them any more; removing them is a proto change and belongs in its own commit.

🤖 Generated with Claude Code

https://claude.ai/code/session_019GHRRFvbnZi5q3KSTvyW5a

aledbf and others added 2 commits September 7, 2026 23:10
spinbox kept its own copy of what a VM is: a command-line builder, a kernel
command line, and a MachineIdentity beside them that spelled the same four
arguments a second time so a template's fingerprint could be computed before
there was a command line to read. The two were kept in step by a shared
machineShape function and a comment asking the next person not to break it,
because nothing checks a restore at run time — a template loaded into a machine
of another shape is memory and device state going into hardware it did not come
from, and it does not fail, it misbehaves.

There is one machine.Spec now. The command line is Args() of it and the
fingerprint is Fingerprint() of it, so the two cannot disagree because there is
nothing left to disagree.

The two definitions had already drifted, in both directions. Ours hashed
runtime.GOARCH, which cannot differ without the QEMU binary differing. It hashed
the host CPU unconditionally, which is right under `-cpu host` and wrong the
moment a model is named — it would partition templates per machine exactly when
the point of naming one is that they cross machines. And it did not hash the
device list at all, so adding the balloon left every existing template matching
a machine it could no longer be restored into.

**Memory grows through virtio-mem.** The `-m` line has no slots any more, so the
pc-dimm path went with them: a backend object and a device per step, a table of
which of the eight slots were in use, LIFO ordering so unplug took the newest, an
RPC into the guest to online what arrived and another to offline what was
leaving, and a rollback for each of the four ways that could half-fail. One
qom-set replaces it. The guest onlines by itself (memhp_default_state=online) and
shrinking is the same call with a smaller number. What the device reports is read
back rather than assumed: a request is a negotiation, and recording the number we
asked for would leave the controller believing in memory the VM does not have.

**Every disk is a qcow2.** The format was worked out from the file extension
while the command line was being built — a guess in the one place nobody looks.
There is one format because there is one thing that produces them: a chain with a
read-only base many VMs map at once and a read-write tip that is this VM's.

**One layout.** The release tree is the layout everywhere now, so hack/spin-machine
copies instead of rearranging, and internal/paths stops guessing: no candidate
lists ending in /usr/bin, no second kernel name. A host with no release used to
run *a* QEMU, with different devices and a different fingerprint, and find out by
way of a guest that would not start.

Deleted rather than adapted: qemu_command_test.go and the fingerprint and
machineShape tests, which now test machine.Spec upstream and better; and
qmp_memory_test.go, whose every test was either a 128 MB alignment rule that no
longer exists or a t.Skip("manual integration test") that has never run.

Verified: task lint, go test -race ./..., and hack/spin-machine against a sibling
checkout.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019GHRRFvbnZi5q3KSTvyW5a
go.mod carried spin-machine as indirect while three files import it; `go mod
tidy` says what is true.

The pin is a branch commit again and not a tag, for the reason the last one was:
spin-stack/spin-machine#2 carries Spec.Identity, which this repository's template
cache keys on, and which the merge of #1 did not take. It moves to a release once
that lands.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019GHRRFvbnZi5q3KSTvyW5a
@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

aledbf and others added 2 commits September 7, 2026 23:26
…t assumed

Hard-coding qcow2 was wrong today. It is where the disks are going — one chain,
a read-only base many VMs map and a read-write tip that is this VM's — and it is
not where they come from: containerd's snapshotter hands this shim a merged.vmdk
and a raw rwlayer.img, and QEMU refused the first of them outright.

    qemu-system-x86_64: -drive file=…/merged.vmdk,…,format=qcow2:
    Image is not in qcow2 format

The guess it replaced was no better — the format was worked out from the file
extension while the command line was being built, by code that had been handed
the answer and dropped it: `MountConfig.Vmdk` was already set by the one party
that knows, and `addDisk` ignored it.

So the option says the format instead of saying a format's name: WithFormat
replaces WithVmdk, DiskConfig carries it, and vm.DefaultDiskFormat is what a disk
is when nobody says otherwise. That constant is raw, and turning it into qcow2 is
the whole of the change when the disks stop coming from a snapshotter.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019GHRRFvbnZi5q3KSTvyW5a
A rename left template_build_integration_test.go calling a function that no
longer existed. `task lint` was green and `go test ./...` was green, because
neither compiles what is behind a build tag; the break surfaced in CI as
`[build failed]` after five minutes of setting up a VM — the slowest lane in the
repository reporting the fastest kind of error there is.

`go vet -tags=integration,e2e` beside the linter. Not golangci-lint with the
tags: the break to catch is a build break, and turning the full linter loose on
files it has never seen reports seven pre-existing style findings that belong to
nobody running this today.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_019GHRRFvbnZi5q3KSTvyW5a
@aledbf
aledbf merged commit e3405dc into main Sep 8, 2026
5 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant