Make host setup complete in one run, and let the installer run it #66
No reviewers
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:cli
scope:drill
scope:host
scope:installer
scope:templates
scope:tiers
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/box#66
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/setup-host-single-run"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes #63. Fixes #64. The two are coupled — #64 is only safe once #63 is true, because an installer that runs
setup-hoston a fresh host hits exactly the halfway-exit that #63 is about.#63 —
setup-hoststopped halfwayWhen it had to add you to
incus-admin, itusermod-ed, printed a NOTE telling you to re-login and re-run, andexit 0-ed. That is a success-shaped no-op: noboxnet, nobox-isolateACL, nobox-netprofile, no firewall — and an exit code that says it all went fine. It now re-execs itself undersg incus-adminand finishes in that same invocation.The membership check was also asking the wrong question, which is the part I did not expect.
id -nG "$USER"names a user, so it reads the group database — which listsincus-adminthe instantusermodreturns, while the running shell's own credentials still lack it (supplementary groups are fixed at login). Measured on a clean box:So the documented workaround — re-run it in the same shell — passed the guard with credentials that still lacked the group, sailed into the
incuscalls, and died on a bare permission error naming neither the group nor the re-login. Arglessid -nGasks the process what it actually holds, which is whatincuschecks when it opens/var/lib/incus/unix.socket.A
BOX_SETUP_HOST_REEXECguard bounds this to one hop: ifsgsomehow lands without the group, it fails loudly rather than forking forever.#64 — the installer now finishes the job
install.shprinted a warning and left you a command to run, so the install reported success andbox newthen failed on a host with no Incus. It now runssetup-hostitself. Sincesetup-hostis idempotent, doing this on every install is also how an upgraded host picks up stack changes — the isolation fixes that ship as new firewall rules land when the tool claiming them lands, instead of waiting on someone to re-run a command.BOX_SKIP_SETUP_HOST=1opts out (CI, image builds, hand-built hosts).</dev/null, because undercurl | bashthis script is stdin. Not theoretical — the counterfactual, tested:sudois unaffected — it prompts on/dev/tty, so an interactive host still authenticates.Verified
Exercised on a clean box, real groups, real
sg, no mocking of the mechanism under test:usermod, no re-exec (idempotent re-run)[--flag] [arg with spaces]sgwithout the group → exit 1, loud, no fork bombcurl … | bashfrom this branch, real network, real tarball → installs,boxruns,INSTALLED_FROMcorrectNot verified, and worth a reviewer with a real host: the stack build itself past the group gate (Incus install,
boxnet, ACL, firewall) — this box is network-isolated from any Incus host, so I stubbed at that boundary and tested everything up to it. The changed lines are the group gate and the installer, but a livebox setup-hoston a genuinely fresh Ubuntu 24.04 / Debian 13 host is the check I could not run. In particularsg incus-adminon a host whoseincuswas just installed in the same run is worth watching.Docs updated: README (which documented the "run twice" workaround verbatim),
box help setup-host, CHANGELOG under Unreleased.🤖 Generated with Claude Code
Two blocking issues from tracing the new installer path:
install.shdeliberately enters setup whenid -u == 0even ifsudois unavailable, butsetup-host.shstill usessudofor every privileged operation. On a minimal root environment this fails immediately:This means the installer's explicit root fallback cannot work on the sort of minimal host it is intended to support. Please make setup-host privilege-aware (e.g. a command prefix that is empty for UID 0 and
sudootherwise), and cover fresh root-without-sudo in the installer/setup tests. Also derive the target login user deliberately rather than addingroottoincus-admin; root does not need socket group access.drill/drill.shwas not updated for the new contract. Stage 1 now invokesinstall.sh, which runs setup and performs its ownsgre-exec. The drill then runs setup-host once before its outersg, and again after it, so a fresh run can perform the stack convergence three times while the messages still describe the old "first pass may only add the group" behavior. That does not isolate or prove the key claim that the installer itself completes setup once. Please update the drill flow to assert the post-install stack directly (and only enter the group for the remainder of the drill), with an explicit opt-out where the drill intentionally owns setup.The process-group check and bounded re-exec direction otherwise look sound, but I cannot agree with the installer integration until its advertised root path works and the end-to-end drill exercises the new behavior without the old duplicate setup sequence.
Both findings were real. I reproduced each before touching anything, and both are fixed in
ce3a0c5.1. Root without sudo — confirmed, exit 127
Reproduced your repro exactly:
You are right that this was mine, not pre-existing: I wrote a branch that deliberately proceeds when
id -uis 0 and then handed off to a script that cannot run as root-without-sudo. The root path was nominal.Privilege is now resolved once — empty at UID 0,
sudootherwise, clear error if neither. Same repro now gets through the gate and into real work (apt-get updaterunning as root with nosudobinary on PATH).Testing this caught a bug in my own fix, which is worth flagging:
With
$SUDOempty, bash has already parsedDEBIAN_FRONTEND=...as a plain word — assignments are recognised at parse time, before expansion — so it tries to execute it:DEBIAN_FRONTEND=noninteractive: command not found. Now$SUDO env DEBIAN_FRONTEND=... apt-get, which is immune either way.Your
SUDO_USERpoint was the better half of the finding and I took it: root does not needincus-admin, so granting it to root was a no-op that also left the actual human locked out. Verified across four paths — normal user (still one-run),sudowithSUDO_USER=claude(grants the human, not root, no re-exec), idempotent re-run, and bare root login (touches no groups).Also bounded the apt calls (
DPkg::Lock::Timeout=300, non-interactive). Not in your review, but it becomes load-bearing the momentinstall.shruns setup unattended: a fresh cloud image hasapt-dailyholding the dpkg lock and a plainapt-get installwaits on it silently and forever. The drill worked around this by pre-installing incus; the installer had no such shield.2. The drill — confirmed, and it mattered more than the count
The triple convergence was the symptom; the real problem is the one you named: the drill ran
setup-hostitself right after installing, so the stack existed by the drill's own hand and a run passed identically whether or notinstall.shhad done a thing. My PR claimed a contract the drill could not see.sgpass is gone (its "first pass: may only add you to incus-admin" message described behaviour #63 deletes).install.shbuilds nothing, and FATALs naming justnft-bridge-boxon a partial stack — the silent-regression shape.DRILL_OWNS_SETUP=1hands sequencing back to the drill.install.sh, since that is what triggers setup now — the nft/ufw one was reading post-setup state and could never fire.One correction: it is now two convergences, not one, and the second is deliberate. The clean deliberately unsets
dns.mode(shipped stack) and reverts old phase-D mutations, so something must converge the host back afterwards. With the assertion ahead of it, that call now proves idempotency instead of hiding the installer.Open question for @danielarturomt — your call, not mine
install.shnow runssetup-hoston every install, including upgrades on a host with live boxes attached to boxnet. drill.sh's own comment says that is how run 6 stalled:So a routine
curl … | bashupgrade could now reconfigure the network under running boxes. My guess is it no-ops when values are unchanged and only pushes when the stack actually changed — i.e. exactly when you want it — but I cannot test that: this box is network-isolated from any Incus host. Two options:box setup-host. Also makes the drill's dirty-host path safe, sinceinstall.shwould skip and the clean→converge ordering stays intact.I lean 1 (security propagation is worth more than a narrow stall on aborted-run hosts) but I would rather you choose than have me quietly redefine your upgrade semantics. Same caveat as before still stands: the stack build past the group gate is unverified on a real host — I stubbed at that boundary and tested everything up to it.
What are the actual consequences of reconfiguring the network of the previous boxes? my ideal would be that the installer is idempotent in nature, but if you are trying to install a conflicting version it should fail, or at the very least we should keep the boxes installation versioned so if you install another version you just change it and it uses the new thing, we could potentially add a migrate for "old" boxes or move or whatever. I feel like the easy escape hatch is to fail loudly if you do the curl install and you already have it installed, or if you have running boxes, or even stopped, they could have something you need, so it just fails very loudly, you can then snapshot the boxes you want to keep, down them, rm them and then run install. then afterwards you do a from to restore the snapshots. And the hard but cool solution is that its just magical, you run it, and if its already the same version it just tells you, if its not, it migrates boxes and everything. I feel like we really want to go the hard route, i'd do it as a separate issue do the hatch first.
Hatch built and pushed (
3e1143f); the magical route is #67. Taking your question first, because it is the one I cannot fully answer."What are the actual consequences of reconfiguring the network of the previous boxes?"
Nobody knows, and I want to be straight that I am reasoning, not reporting. I have no Incus host — this box is network-isolated from one — so everything below is read off the code and your own drill notes.
An upgrade re-runs three things against a live
boxnet:incus network set boxnet security.acls=…incus network set boxnet dns.mode=noneincus network set boxnet raw.dnsmasq …So my expectation is: no data loss and no box destroyed (instances are never recreated), but a transient DNS/connectivity blip inside running boxes, and a stall risk when the ACLs genuinely change. I also suspect Incus no-ops when values are identical, which would make the common upgrade free — but that is exactly the guess I would be encoding, and run 6 is evidence that it is not always free.
The measurement that settles it is a drill phase that mints boxes, upgrades under them, and asserts they survive. I wrote that up in #67 rather than pretend I had done it.
The hatch (this PR)
Built as you specified, with
VERSION+ref as the identity:already at 0.5.0 (…) — nothing to migrate, verified on a realcurl | bashre-run)$DESTis touched so the working install survives the refusalBOX_FORCE_UPGRADE=1overrides. The drill sets it — arriving on a dirty host and wiping it is its job.Two things I want to flag rather than bury:
Ref, not just VERSION. A branch and
maincarry the sameVERSIONstring, so a VERSION-only check would call an install of this very branch "unchanged" and skip the hatch entirely. Both tag generations count as boxes, too — a pre-renameuser.claudebox=1box is just as much someone's work.The query runs unprivileged first, escalating only if the socket refuses. Anyone who owns boxes is already in
incus-admin, and an installer should not demand a sudo password merely to look.One correction to the recipe — it loses data
This does not work today, and
box rm --helpis the one saying so:A snapshot does not outlive its box, so the
rmdestroys exactly what thefromwas meant to restore. There is no export/publish in the CLI that survives the box either. So the error message does not print that path — it says to copy what you need out first (box shell/box exec), and #67 carries the open question of what the real preservation path should be. If you intendedbox new --from <box>to clone to a keeper before the rm, that clone still lives on the same host and gets the same stack rebuilt under it, so it does not buy anything either.Standing caveat, unchanged: the stack build past the group gate is still unverified on real hardware. Everything I have claimed on this PR is tested up to that boundary and stubbed beyond it.
Are you saying that I can't keep a snapshot/clone that survives after the box is deleted? that's crazy and should be fixed then. A network "blip" is really nothing out of this world, especially during an upgrade. The QoL improvement is real if we can have a single idempotent install that just magically works. now, on the other hand you should be able to run drill yourself in your host.
@danmt — you were right on both counts, and the second one changes this PR materially.
First: I owe you a correction on the snapshot claim
A clone does survive deleting its source.
box new --from <box>/<snap>isincus copy(bin/box:incus copy "$srcref" "$instance") — a real, independent instance. So this works today:What does not survive is a snapshot as a snapshot:
box rmdeletes the box and every snapshot it has, and there is noexport/publishin the CLI, so nothing survives the host. That is the real gap, and it is narrower than what I said. I conflated "survivesrm" with "survives the stack rebuild" in the same sentence and stated it too broadly. Sorry — #67 has the accurate version.Second: you were right that I could run the drill here, and it changes what I can claim
This box is a KVM VM with
/dev/kvm. I had been asserting I could not verify the stack build; I was wrong, and I should have checked instead of caveating. The drill is running against this branch on a real Incus right now. What it has already proven, live — every one of these was previously stubbed:/dev/kvm present — boxes will be VMs (the real trust boundary). Currently 29 PASS / 2 FAIL, still minting.The #63 bug, reproduced on real hardware
I ran the pre-PR
setup-host.shon this host, in a shell that was inincus-adminin the group DB but not in its own credentials — i.e. exactly the "run it again" the README prescribed:Not theory. It also mislabels the cause — that is a permission failure being reported as "btrfs preseed failed", so the old code would tell you your host cannot do btrfs when you were merely not in the group. The new code, same shell, same state:
added claude to incus-admin — re-running under the new group→Host ready.→ exit 0.A real bug the drill found (
043e556)box-firewall.serviceisType=oneshotwith noRemainAfterExit=yes, so it goesinactive (dead)the moment it succeeds. I hit this myself: the box-to-box drop was live innft list table bridge boxwhilesystemctl is-activesaidinactive. drill.sh's own failure hint sends you tosystemctl status box-firewall.serviceto diagnose the firewall — so the healthy state and the alarming one are indistinguishable. And setup-host.sh already assumed the line was there ("The unit is RemainAfterExit, so once it has run it stays active forever") and reasons from it. The reasoning and therestartare right; the unit was missing the line. Fixed and verified live (active (exited), drop still present, restart still re-applies). Say the word if you want it split out.On the blip
Noted — you own that call and it is your project. I will drop the hatch and go idempotent-always if you want it; the code is small either way. One thing I would keep separate: a blip is not the stall drill.sh blames for run 6. Those are different failure modes and I have still only measured neither. If you want, I can extend the drill to mint boxes, upgrade under them, and report what actually happens — that turns your "a blip is nothing out of this world" from a reasonable prior into a measured fact, and it is the thing #67 needs anyway.
Two drill FAILs — neither is from this PR, reporting them straight
blank box has NO egress — isolation parity broken. I do not believe this one. The egress probe isbox_pings tpl 1.1.1.1(ICMP), while the DNS probe next to it isgetent(libc) — and DNS passed. Meanwhile DOCKER-USER shows9621packets accepted from boxnet and221Mbytes to it: egress demonstrably works, that is the template installs downloading. The blank template is bareimages:debian/13/cloudwithpackage_update: false, so my hypothesis is the image simply has nopingand the probe is testing for the wrong thing. Confirming after the drill.codex: 'codex --version' FAILED via exec — not installed(anything named codex on disk:empty). cloud-init reportedstatus: done. The drill itself labels this shape "the claude template's #15 bug".Full results when it finishes.
then file an issue for the export command so the snapshot does survive. We'll stick to the escape hatch first. While I think a small blip is no big deal is hard to ensure that will be the only potential problem. Lets do the fail loudly so you have to manually stop, export, remove every box before you run it again. in order for install to work it has to make sure there are no active boxes that can be potentially lost.
The user runs the
curlcommand from the README. It asks the user to confirm if they want to install, thats the first thing. if you confirm it checks if you already have it installed, if you do, it tells you and finishes. If you dont have it installed it does that part of the process, then it confirms via another prompt that the user wants to setup the machine as a host, if you say yes it does it (even the re-exec) and you end up with a proper box host. If you already had box installed it will be a no-op, so it dissolves an entire class of errors. If you want to install a new version, you uninstall the one you have (this is not ideal but its a good starting point).Built your flow (
d0a14a1), filed the export issue (#70), and the drill is re-running against it on the real host now. Also chased down the two drill FAILs — one is a false alarm, and I can tell you why.The installer, as you described it
Re-running is now a no-op the moment box is installed, exactly as you said — that dissolves the whole "curl clobbered my install / rebuilt the stack under my boxes" class, and it does it without the installer having to reason about versions or enumerate boxes at all. You cannot lose a box to an install that refuses to touch an existing one. Upgrading is explicit: uninstall, install fresh. I replaced last round's version-diff refusal with this; it is simpler and covers the same ground.
#67(migrate) and#70(export) carry the "make upgrade painless" work.Verified on the real host, all five paths: cancel; fresh install; no-op re-run; and both prompts driven through a real pty (
y/nandy-then-n).One decision you did not specify, so I want it visible
curl … | bashhas no stdin to read — the script is stdin. So the prompts read/dev/ttydirectly. On a machine with no terminal at all (CI, an image build, the drill), there is nobody to ask, soBOX_YES=1means "assume yes to every prompt" and is required to proceed unattended; without it, it refuses rather than invent your consent.BOX_SKIP_SETUP_HOST=1answers just the host-setup prompt "no". If you would rather the no-tty default be "yes" instead of "refuse", that is a one-line change — say the word.The two drill FAILs — one real-ish, one a false alarm
blank box has NO egress— false alarm, and it is the probe that is wrong, not the box. I mistrusted it because DNS passed on the same box, so I probed egress 8 times in a row on a fresh blank box:Egress flaps — ~50% loss to the public internet, while DNS (which resolves at the gateway, 10.88.0.1, one hop away) is rock solid. That is the signature of nested virtio networking: I am running the drill inside a KVM guest, and the boxes are VMs nested inside that, so box→internet is double-NATed through two virtio hops and drops packets. On your real metal this almost certainly will not reproduce. But it did expose a genuine drill weakness:
box_pings tpl 1.1.1.1is a single ICMP shot, so one dropped packet = a red FAIL. Worth making that probe retry (say 3 tries) so a lossy link does not cost a false isolation-parity failure. Happy to do it here or as its own issue — your call, since it is drill-only.codex: codex --version FAILED— real, but pre-existing and not this PR. The drill's own label calls it "the claude template's #15 bug":codexis not onPATH(nor on disk) after a cold mint even though cloud-init reportedstatus: done. It is a template/PATH issue, untouched by anything here. Flagging, not fixing, unless you want it chased.And a real bug the real host surfaced (already pushed,
043e556)box-firewall.servicewasType=oneshotwith noRemainAfterExit=yes, so a perfectly healthy firewall reportedinactive (dead)— and drill.sh sends you to exactly that command to diagnose the firewall. Fixed;setup-host.sh's own comment had already assumed the line was present. Details two comments up.Status of the thing I kept caveating
The entire #63/#64 chain is now proven on real hardware, not stubs: I reproduced the old code's
permission deniedfailure on this host, watched the new code one-run through it, and the drill's stack assertion passes green. The one gap left is theclaudetemplate's ~10-min cold mint, which crashes here on a nested-VMvsockreset at 7GiB RAM — environmental, not code. Everything up to it is green.There should be a copy-pasteable command snippet in the description of this PR that can be used to probe the changes. Running drill against this branch.