fix(vm): sync the new runtime into var instead of destroying it - #212
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR changes the VM update path to preserve the live /var disk on seed changes by scheduling a guest-side sync of Avocado-managed state (via a read-only attached seed) instead of deleting/reseeding var.btrfs. It also fixes systemd unit enabling in extension builds by honoring [Install] directives coming from drop-ins.
Changes:
- Record
runtime.pending_var_seed_shaduringvm updateand attach the corresponding seed read-only onvm start, passing both seed sha and btrfs fsid to the guest via kernel cmdline. - Add host-side btrfs fsid extraction (fixed-offset read) and best-effort clearing of the pending marker only after the guest confirms the seed was applied.
- Update extension build service-enabling logic to read
[Install]from drop-ins (and add an integration-style test that runs the emitted shell fragment).
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/vm/qemu.rs | Adds VarSeed and appends a read-only seed -drive; passes seed identifiers on the kernel cmdline; adds ordering/shape tests. |
| src/utils/vm/lifecycle.rs | Resolves pending seed attachment, reads btrfs fsid from the seed image, and clears pending_var_seed_sha after guest confirmation. |
| src/utils/vm/config.rs | Adds persisted runtime.pending_var_seed_sha config field and updates config round-trip tests. |
| src/main.rs | Removes --reset-var flag plumbing and updates CLI help text to reflect non-destructive updates. |
| src/commands/vm/update.rs | Switches from destructive var reset to recording a pending seed sync; updates prompt behavior and JSON event. |
| src/commands/ext/build.rs | Parses [Install] from unit drop-ins when enabling services and adds a test that executes the generated shell fragment. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Builds on the var-reset branch and replaces the reset. Deleting var.btrfs on a seed change also deletes /var/lib/docker, which holds the SDK image layers and the avo-<uuid> volume that *is* $AVOCADO_PREFIX — so every update cost a full SDK re-pull and sysroot rebuild, and the release notes had to tell people to re-run `avocado install` and `avocado build`. That was never necessary. The avocado-owned half of var is already content-addressed and versioned (images/, runtimes/, active), and avocadoctl already installs a runtime alongside the current one and switches `active` only once it is staged and verified. So the guest can adopt the release's runtime out of the new seed and leave everything else untouched. The btrfs work has to happen guest-side regardless: this VM exists because the host is macOS or Windows, neither of which can mount a btrfs image. - `vm update` records runtime.pending_var_seed_sha rather than unlinking the disk. - `vm start` attaches that seed read-only and names its sha on the cmdline; the guest echoes it into a stamp, which is what lets the marker be cleared for this sync specifically. Every failure leaves it set, so retry is the whole recovery story. - The seed is addressed by serial rather than /dev/vdN: the var and data drives are both conditional, so enumeration order is not stable across boots or arches. - Never attached when the live disk was byte-copied from that same seed — identical btrfs fsid, which the kernel reads as one multi-device filesystem. Nothing is destroyed, so --reset-var, the typed-'update' prompt and the var_reset event go away; the JSON event becomes var_seed_sync_pending, which host applications can ignore rather than acting on. record_var_size_floor goes with them — it existed only so a grown disk survived the delete. Requires the guest-side unit in avocado-vm; without it the drive is attached and nothing reads it, which is exactly today's behaviour.
`enable_services` parsed WantedBy=/RequiredBy= out of the unit file alone. systemd also reads [Install] from drop-ins, and `systemctl enable` honours a WantedBy= declared there — so the standard way to enable a vendor unit that ships without an [Install] was invisible here, and the build failed with the fix already sitting in the overlay. Hit while building avocado-vm against distro 2026, where qemu-guest-agent.service no longer carries an [Install] section. Parsed one file at a time: a single sed over several files shares an input stream, so the /^\[Install\]/,/^\[/ range would run past the end of one file and match keys in the next. Test runs the generated fragment with sh against a real sysroot holding a unit with no [Install] plus a drop-in supplying one, and asserts the .wants symlink appears — string assertions can't catch a shell bug in generated code. Fails against the previous parser.
… UUID Found by running the upgrade on a real VM: it dropped into emergency mode, with btrfs on /dev/vda where the erofs rootfs belongs. `serial=` needs `if=none` plus an explicit `-device`, and an explicit `-device virtio-blk-pci` takes a lower PCI slot than the devices qemu creates for `if=virtio`. So the seed became /dev/vda and pushed the rootfs and var disks along, breaking both `root=/dev/vda` on the cmdline and `/dev/vdb /var` in the guest fstab. Addressing the seed by serial fixed the guest's lookup and broke the two consumers that address disks by position. Keep every drive on `if=virtio` so attaching one is purely additive, and identify the seed by its btrfs filesystem UUID instead, passed as avocado.seed_fsid= alongside the sha. Reading that UUID is a 16-byte read at a fixed superblock offset rather than a shell out to blkid: the hosts this VM exists for are macOS and Windows, which have neither blkid nor btrfs. Nothing here is cfg-gated, so it behaves the same on all three. Tests pin the ordering property directly — no seed emits one if=virtio drive, a seed emits exactly two, the seed never precedes the rootfs, and no explicit -device appears.
All four from Copilot, all real: - pending_seed_sha was derived before the btrfs_fsid filter, so a seed whose UUID cannot be read (truncated, corrupt) was never attached yet still interrogated the guest for an application that could not have happened, silently, on every start. The sha now comes from the VarSeed actually attached, and the unreadable-seed case warns loudly instead of retrying quietly forever. - The [Install]-parsing shell accumulated file paths into one space-separated string and re-split it, breaking on any path with a space. Each file is now parsed as encountered through a quoted "$1". - vm/update.rs still carried the delete-the-disk comment from the superseded reset approach, describing destruction the code no longer performs. - The seed-drive test claimed addressing by virtio serial; the mechanism is (deliberately) the filesystem UUID. Renamed to pin what it actually asserts.
mobileoverlord
force-pushed
the
jschneck/vm-seed-sync
branch
from
August 25, 2026 11:38
8f064ef to
23928ed
Compare
The no_stdout_on_the_vm_path guard landed on main after this branch forked and rightly flags this site: it runs on the vm start path where the following command may own stdout.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #196 — targets
fix-vm-update-reset-varso the diff reads as a delta. Squashing into #196 is fine; the two are one change in intent.Why this replaces the reset
#196 deletes
var.btrfson a seed change. That also deletes/var/lib/docker, which holds the SDK image layers and theavo-<uuid>volume that is$AVOCADO_PREFIX— hence the release note telling people to re-runavocado installandavocado build.That was never necessary. The avocado-owned half of var is already content-addressed and versioned (
images/,runtimes/,active), andavocadoctlalready installs a runtime alongside the current one and switchesactiveonly once it is staged and verified. The guest can adopt the release's runtime out of the new seed and leave everything else alone.The btrfs work has to happen guest-side regardless: this VM exists because the host is macOS or Windows, neither of which can mount a btrfs image.
Change
vm updaterecordsruntime.pending_var_seed_shainstead of unlinking the disk.vm startattaches that seed read-only and names its sha and filesystem UUID on the cmdline. The guest echoes the sha into a stamp, which is what lets the marker be cleared for this sync. Every failure leaves it set, so the next start retries — that is the whole recovery story.seed_var_diskis astd::fs::copy, so the two carry an identical btrfs fsid and the kernel would read them as one multi-device filesystem. A correctness gate, not an optimisation.The ordering bug this found
The first version addressed the seed by
serial=, which needsif=noneplus an explicit-device. An explicit-device virtio-blk-pcitakes a lower PCI slot than the devices qemu creates forif=virtio— so the seed became/dev/vda, the rootfs and var disks shifted, and the VM dropped into emergency mode withroot=/dev/vdapointing at a btrfs seed. Fixing the guest's lookup broke the two consumers that address disks positionally: the kernel cmdline and the guest fstab.Every drive now stays on
if=virtioso attaching one is purely additive, and the guest finds the seed by filesystem UUID. Reading that UUID is a 16-byte read at a fixed superblock offset rather than shelling out toblkid— the hosts this VM exists for have neither blkid nor btrfs. Nothing is cfg-gated; it behaves the same on macOS, Linux and Windows.Tests pin the ordering property directly: no seed emits one
if=virtiodrive, a seed emits exactly two, the seed never precedes the rootfs, and no explicit-deviceappears.Also included
ext/build.rsnow reads[Install]from drop-ins, not just the unit body. systemd honours aWantedBy=declared in a drop-in, so the standard way to enable a vendor unit shipping without an[Install]was invisible to the build. Parsed one file at a time — a singlesedover several files shares an input stream, so the[Install]range would run past a file boundary. Test runs the generated fragment withshagainst a real sysroot and asserts the.wantssymlink appears; it fails against the previous parser.What this removes from #196
Nothing is destroyed, so
--reset-var, the typed-updateprompt and thevar_resetevent all go. The JSON event becomesvar_seed_sync_pending, which host applications can ignore rather than act on.record_var_size_floorgoes too — it existed only so a grown disk survived the delete.min_cli_version— the most valuable part of #196 — is untouched.Testing
1412 unit tests, clippy clean.
End to end on a real VM: stock 0.3.0 migrated to a locally built release, 8/8 assertions — var.btrfs the same inode throughout, active runtime changed, all extensions merged, and the user's Docker volume, image layers and
/varmarker intact. Kernel 6.6.123 → 6.18.35, extensions 2024.1.0 → 0.1.0.Depends on
avocado-vm#3 (guest unit) and avocado-vm#4. Without #3 the drive is attached and nothing reads it — exactly today's behaviour, so they can land in either order.
Reopened as a new PR: #206 was auto-closed when its stack base (#196, now merged) was deleted, and GitHub refuses to reopen across a deleted base. Branch rebased onto main (3259 tests pass); the #196 commits dropped out as already-merged.