feat(cli): versions, use, uninstall — the install managed from the CLI, absence-asserted
'box versions' lists what is installed (current + running marked); 'box use' flips the default, refuses under existing boxes (#66), and asserts the EFFECTIVE result — current resolves to the version asked for and the chain's own --version answers it. 'box uninstall' is the real uninstall the old two-rm-rf-lines of prose never were: one version, or everything in the safe order (boxes first — refuse or --purge-host — then trees and symlinks), and it ends by re-checking every promised path is gone, exiting 1 naming leftovers (the revoke --purge discipline). teardown-host grows --yes/BOX_YES for the unattended paths and points at 'box uninstall'; the drill reads the installed tree through current/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6e92853925
commit
9d3ca92ffa
3 changed files with 306 additions and 19 deletions
276
bin/box
276
bin/box
|
|
@ -77,6 +77,9 @@ CMDS=(
|
|||
"revoke^<user> [--purge]^^Admin: take the restricted tier back (--purge also deletes their boxes)^fn:cmd_revoke^"
|
||||
"teardown-host^[--purge-incus]^^Remove the box host stack (both name generations)^fn:cmd_teardown_host^"
|
||||
"migrate-host^--box <n> | --all-boxes | --retire-legacy^^Move a host from the pre-0.4.0 stack onto box^fn:cmd_migrate_host^"
|
||||
"versions^^^List the installed box versions — the current default and the running one^fn:cmd_versions^"
|
||||
"use^<version>^^Switch the default box version (refuses while boxes exist)^fn:cmd_use^"
|
||||
"uninstall^[<version>] [--all] [--purge-host]^^Remove one installed version, or the whole install — asks first, asserts the absence^fn:cmd_uninstall^"
|
||||
"status^^^Deprecated alias for 'list'^fn:cmd_status^"
|
||||
"help^[<command>]^^This help, or 'box help <command>' for one command^fn:cmd_help^"
|
||||
)
|
||||
|
|
@ -480,6 +483,56 @@ the profile, then verifies the box works on its new network leg.
|
|||
box migrate-host --all-boxes # re-home every legacy box
|
||||
box migrate-host --retire-legacy # remove the old stack (once no legacy box remains)
|
||||
EOF
|
||||
;;
|
||||
versions) cat <<'EOF'
|
||||
List the versions installed under this install root — install.sh lands each
|
||||
one side by side at <root>/versions/<v>, and a 'current' symlink tracks the
|
||||
default (what the box on your PATH runs). The default is marked (current);
|
||||
the tree answering THIS command is marked (running) — they differ when your
|
||||
PATH resolves a different install (say, a global /opt/box shadowing yours).
|
||||
|
||||
box versions
|
||||
box use <version> # switch the default
|
||||
# install another version side by side: re-run install.sh
|
||||
EOF
|
||||
;;
|
||||
use) cat <<'EOF'
|
||||
Switch the default box version — repoint the 'current' symlink (and the PATH
|
||||
symlink riding it) at an installed version. Refuses while ANY box exists:
|
||||
never change versions under a user's boxes (#66) — 'box down' what you keep,
|
||||
copy out what you need via 'box shell'/'box exec' (a portable 'box export'
|
||||
is #70), 'box rm' each box, then switch. The flip is asserted afterwards:
|
||||
current must resolve to the version you asked for, and the chain's
|
||||
'box --version' must answer it.
|
||||
|
||||
box versions # what is installed
|
||||
box use 0.6.0
|
||||
EOF
|
||||
;;
|
||||
uninstall) cat <<'EOF'
|
||||
Remove one installed version, or the whole install — the real uninstall,
|
||||
replacing the old "rm -rf two paths by hand" instructions.
|
||||
|
||||
box uninstall <version> one NON-current version ('box use' another
|
||||
first if you are on it)
|
||||
box uninstall everything: every version, the current and
|
||||
box uninstall --all PATH symlinks, and any legacy claudebox crumbs
|
||||
box uninstall --purge-host run teardown-host first (all boxes, the boxnet
|
||||
stack, the firewall — its own confirmation),
|
||||
then remove the install
|
||||
|
||||
The full uninstall runs in the safe order: boxes first — it refuses while
|
||||
any exist (and names them) unless --purge-host tears them down; then the
|
||||
trees and symlinks; and it ENDS with an absence assert — every removed path
|
||||
is re-checked, and anything still present makes it exit 1 naming the
|
||||
leftovers instead of reporting a clean uninstall that wasn't (the same
|
||||
discipline as 'box revoke --purge'). Asks before removing; --force or
|
||||
BOX_YES=1 skips the prompt. On a multi-user host, revoke granted users
|
||||
first: 'box revoke <user> --purge'.
|
||||
|
||||
box uninstall 0.5.0
|
||||
box uninstall --all --purge-host
|
||||
EOF
|
||||
;;
|
||||
status) cat <<'EOF'
|
||||
Deprecated alias for 'box list'. It ignored the <box> argument it
|
||||
|
|
@ -536,8 +589,9 @@ while [ $# -gt 0 ]; do
|
|||
if [ "$cmd" = doctor ]; then args+=("$1"); shift; continue; fi
|
||||
# expose's own flags (--list, --remove) are positional to it, not box's
|
||||
if [ "$cmd" = expose ]; then args+=("$1"); shift; continue; fi
|
||||
# the host verbs delegate their flags to the scripts they wrap
|
||||
case "$cmd" in setup-host|teardown-host|migrate-host|grant|revoke) args+=("$1"); shift; continue ;; esac
|
||||
# the host verbs delegate their flags to the scripts they wrap;
|
||||
# uninstall parses its own (--all, --purge-host) the same way
|
||||
case "$cmd" in setup-host|teardown-host|migrate-host|grant|revoke|uninstall) args+=("$1"); shift; continue ;; esac
|
||||
if [ "$cmd" = exec ] || [ "$cmd" = incus ]; then
|
||||
usage_error "unknown option: $1 — a command's own flags go after --, as in '$(synopsis_of "$cmd")'"
|
||||
fi
|
||||
|
|
@ -1045,6 +1099,224 @@ cmd_migrate_host() { host_script migrate-host.sh; }
|
|||
cmd_grant() { host_script grant-user.sh; }
|
||||
cmd_revoke() { host_script revoke-user.sh; }
|
||||
|
||||
# --- the versioned install (#66's stance, made livable in 0.7.0) ------------
|
||||
# install.sh lands each version at <install-root>/versions/<v>, with a
|
||||
# 'current' symlink naming the default and $BINDIR/box pointing through it.
|
||||
# $root (readlink -f, line 8) already resolved the whole chain, so a versioned
|
||||
# install always runs from .../versions/<v> — and a git checkout does not,
|
||||
# which is how these verbs know to refuse instead of uninstalling somebody's
|
||||
# working copy.
|
||||
install_root() {
|
||||
local vdir; vdir="$(dirname "$root")"
|
||||
[ "$(basename "$vdir")" = versions ] || return 1
|
||||
dirname "$vdir"
|
||||
}
|
||||
|
||||
# Which boxes exist on this host, at THIS caller's tier? Prints their names
|
||||
# (both tag generations) and succeeds when at least one exists; fails when
|
||||
# none are visible — including when incus is absent or not answering, because
|
||||
# #66's stance protects BOXES from a version change, and a daemon that cannot
|
||||
# answer has none to protect. install.sh carries a byte-identical copy (it
|
||||
# runs before any install tree exists); test/cli.sh diffs the two so they
|
||||
# cannot drift.
|
||||
existing_boxes() {
|
||||
command -v incus >/dev/null 2>&1 || return 1
|
||||
{ timeout 10 incus list user.box=1 --format csv --columns n </dev/null
|
||||
timeout 10 incus list user.claudebox=1 --format csv --columns n </dev/null
|
||||
} 2>/dev/null | awk -F, 'NF && !seen[$1]++ { print $1 }' | grep .
|
||||
}
|
||||
|
||||
# #66, kept at flip time: never change (or remove) the version under existing
|
||||
# boxes. Names every box and the remedy, then dies — a refusal that does not
|
||||
# say which boxes block it sends the operator off to rediscover 'box list'.
|
||||
die_under_boxes() { # $1 = the act being refused, $2 = the retry command
|
||||
local names n
|
||||
names="$(existing_boxes)" || return 0
|
||||
{
|
||||
echo "box: this host has existing boxes:"
|
||||
while IFS= read -r n; do echo "box: · $n"; done <<<"$names"
|
||||
echo "box: refusing to $1 under them (#66: never change versions under a user's boxes)."
|
||||
echo "box: preserve what you care about — 'box down <box>', copy out via 'box shell'/'box exec'"
|
||||
echo "box: (a portable 'box export' is #70) — then 'box rm <box>' each, and re-run: $2"
|
||||
} >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# The PATH symlinks that could ride this install: the one this invocation came
|
||||
# in on, BOX_BIN's, and the tier default's. Candidates only — every consumer
|
||||
# checks where a link actually points before touching it, so a symlink that is
|
||||
# somebody else's (another install root, a hand-rolled wrapper) is never moved.
|
||||
bin_links() {
|
||||
local c=()
|
||||
[ -L "${BASH_SOURCE[0]}" ] && c+=("${BASH_SOURCE[0]}")
|
||||
[ -n "${BOX_BIN:-}" ] && c+=("$BOX_BIN/box")
|
||||
if [ "$(id -u)" -eq 0 ]; then c+=(/usr/local/bin/box); else c+=("$HOME/.local/bin/box"); fi
|
||||
printf '%s\n' "${c[@]}" | awk '!seen[$0]++'
|
||||
}
|
||||
|
||||
converge_bin_links() { # $1 = install root: point our PATH symlinks through current
|
||||
local ir="$1" p t
|
||||
while IFS= read -r p; do
|
||||
[ -L "$p" ] || continue
|
||||
t="$(readlink -f "$p" 2>/dev/null || true)"
|
||||
[ -n "$t" ] || t="$(readlink "$p" 2>/dev/null || true)"
|
||||
case "$t" in
|
||||
"$ir"/*) ln -sfn "$ir/current/bin/box" "$p" ;;
|
||||
esac
|
||||
done < <(bin_links)
|
||||
}
|
||||
|
||||
cmd_versions() {
|
||||
local ir cur d v mark
|
||||
ir="$(install_root)" || die "this box runs from a working tree ($root), not a versioned install — nothing to list"
|
||||
cur="$(readlink -f "$ir/current" 2>/dev/null || true)"
|
||||
echo "VERSIONS ($ir)"
|
||||
for d in "$ir/versions"/*/; do
|
||||
[ -d "$d" ] || continue
|
||||
v="$(basename "$d")"
|
||||
mark=""
|
||||
[ "$(readlink -f "$d")" = "$cur" ] && mark=" (current)"
|
||||
[ "$(readlink -f "$d")" = "$root" ] && mark="$mark (running)"
|
||||
printf ' %s%s\n' "$v" "$mark"
|
||||
done
|
||||
echo
|
||||
echo "switch the default: box use <version>"
|
||||
echo "install another: re-run install.sh (versions land side by side)"
|
||||
}
|
||||
|
||||
cmd_use() {
|
||||
local v="${args[0]:-}" ir eff expect out
|
||||
[ -n "$v" ] || usage_error "usage: $(synopsis_of use)"
|
||||
ir="$(install_root)" || die "this box runs from a working tree ($root), not a versioned install — nothing to switch"
|
||||
[ -d "$ir/versions/$v" ] || die "no such version: $v (see 'box versions')"
|
||||
die_under_boxes "switch the default box version" "box use $v"
|
||||
ln -sfn "versions/$v" "$ir/current"
|
||||
converge_bin_links "$ir"
|
||||
# Assert the EFFECTIVE result, not the intent: current must resolve to the
|
||||
# version asked for, and the chain's own binary must answer that version —
|
||||
# a flip that "worked" while the operator's box still runs the old tree is
|
||||
# exactly the flakiness this verb exists to end.
|
||||
eff="$(basename "$(readlink -f "$ir/current" 2>/dev/null || true)")"
|
||||
[ "$eff" = "$v" ] || die "the flip did not take — current resolves to '${eff:-nothing}', not $v"
|
||||
expect="$(cat "$ir/versions/$v/VERSION" 2>/dev/null || true)"
|
||||
if [ -n "$expect" ]; then
|
||||
out="$("$ir/current/bin/box" --version 2>&1 || true)"
|
||||
case "$out" in
|
||||
*"$expect"*) : ;;
|
||||
*) die "current/bin/box answers '$out', not version $expect — the symlink chain is broken" ;;
|
||||
esac
|
||||
fi
|
||||
echo "box: switched to $v (current -> versions/$v)"
|
||||
}
|
||||
|
||||
# The uninstall's own confirmation. NOT confirm() above: that one is for box
|
||||
# lifecycle verbs and must never auto-accept from the environment (the drill
|
||||
# exports BOX_YES=1 for the installer and still expects 'box rm' to refuse
|
||||
# without --force). Uninstalling is installer-family, and BOX_YES is the
|
||||
# installer-family consent contract — same as install.sh and revoke --purge.
|
||||
uninstall_confirm() { # $1 = question. --force, or BOX_YES=1, or a TTY.
|
||||
[ "$force" -eq 1 ] && return 0
|
||||
[ -n "${BOX_YES:-}" ] && return 0
|
||||
[ -t 0 ] || usage_error "refusing to $1 without --force (no terminal to confirm on; BOX_YES=1 also means yes)"
|
||||
local reply
|
||||
printf 'box: %s? [y/N] ' "$1"
|
||||
read -r reply
|
||||
case "$reply" in y|Y|yes|YES|Yes) return 0 ;; *) die "aborted." ;; esac
|
||||
}
|
||||
|
||||
# 'box uninstall' — the real uninstall #66 left as two rm -rf lines of prose.
|
||||
# The full removal runs in the documented order: boxes first (refuse while
|
||||
# they exist, or --purge-host tears the stack down with them), then trees and
|
||||
# symlinks, and it ENDS by PROVING the absence — like revoke --purge, the
|
||||
# last word is a re-check, not a hope.
|
||||
cmd_uninstall() {
|
||||
local ir a ver="" all=0 purge_host=0 cur p t granted leftover=""
|
||||
local targets=()
|
||||
for a in ${args[@]+"${args[@]}"}; do
|
||||
case "$a" in
|
||||
--all) all=1 ;;
|
||||
--purge-host) purge_host=1 ;;
|
||||
-*) usage_error "unknown option: $a (see 'box help uninstall')" ;;
|
||||
*) [ -z "$ver" ] || usage_error "usage: $(synopsis_of uninstall)"; ver="$a" ;;
|
||||
esac
|
||||
done
|
||||
ir="$(install_root)" || die "this box runs from a working tree ($root), not a versioned install — nothing to uninstall (a checkout is removed with plain rm)"
|
||||
[ -w "$ir" ] || die "cannot write $ir — a global install is uninstalled as root: sudo box uninstall"
|
||||
|
||||
# -- one version -----------------------------------------------------------
|
||||
if [ -n "$ver" ] && [ "$all" -eq 0 ]; then
|
||||
[ "$purge_host" -eq 0 ] || usage_error "--purge-host goes with the full uninstall, not a single version"
|
||||
[ -d "$ir/versions/$ver" ] || die "no such version: $ver (see 'box versions')"
|
||||
cur="$(basename "$(readlink -f "$ir/current" 2>/dev/null || true)")"
|
||||
[ "$ver" != "$cur" ] || die "$ver is the CURRENT version — 'box use <other>' first, or 'box uninstall --all' for everything"
|
||||
uninstall_confirm "remove box version $ver from $ir"
|
||||
# rm's exit code is not the verdict — the absence re-check below is (a
|
||||
# half-removed tree must be reported as INCOMPLETE, not as a crash).
|
||||
rm -rf "$ir/versions/$ver" || true
|
||||
if [ -e "$ir/versions/$ver" ] || [ -L "$ir/versions/$ver" ]; then
|
||||
echo "box: uninstall INCOMPLETE — still present: $ir/versions/$ver" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "box: removed version $ver (the default stays $cur)"
|
||||
return 0
|
||||
fi
|
||||
[ -z "$ver" ] || usage_error "usage: $(synopsis_of uninstall) — a version and --all together is ambiguous"
|
||||
|
||||
# -- everything ------------------------------------------------------------
|
||||
if [ "$purge_host" -eq 1 ]; then
|
||||
# Granted users' worlds are not ours to erase silently — name them first;
|
||||
# 'box revoke <user> --purge' is the clean path (and asserts its absence).
|
||||
granted="$(timeout 10 incus project list --format csv 2>/dev/null </dev/null | cut -d, -f1 | grep '^user-' | tr '\n' ' ' || true)"
|
||||
[ -n "${granted% }" ] && echo "box: NOTE — granted users still have projects (${granted% }) — 'box revoke <user> --purge' removes each world cleanly first" >&2
|
||||
bash "$root/host/teardown-host.sh" \
|
||||
|| die "teardown-host did not complete — stopping BEFORE removing the install (the tree is untouched; fix the error and re-run)"
|
||||
else
|
||||
die_under_boxes "uninstall box" "box uninstall (or 'box uninstall --purge-host' to tear the host stack down with them)"
|
||||
fi
|
||||
uninstall_confirm "remove the ENTIRE box install at $ir (every version)"
|
||||
|
||||
# The removal set, gathered BEFORE anything is deleted, so the absence
|
||||
# assert below re-checks exactly what was promised gone. PATH symlinks are
|
||||
# removed only when they resolve into (or dangle at) THIS install root.
|
||||
targets+=("$ir")
|
||||
while IFS= read -r p; do
|
||||
[ -L "$p" ] || continue
|
||||
t="$(readlink -f "$p" 2>/dev/null || true)"
|
||||
[ -n "$t" ] || t="$(readlink "$p" 2>/dev/null || true)"
|
||||
case "$t" in "$ir"/*) targets+=("$p") ;; esac
|
||||
done < <(bin_links)
|
||||
# Legacy crumbs: the pre-0.4.0 command name and the pre-0.5.0 tree. A real
|
||||
# uninstall leaves neither generation behind.
|
||||
while IFS= read -r p; do
|
||||
p="$(dirname "$p")/claudebox"
|
||||
[ -L "$p" ] && targets+=("$p")
|
||||
done < <(bin_links)
|
||||
[ -d "$HOME/.local/share/claudebox" ] && targets+=("$HOME/.local/share/claudebox")
|
||||
mapfile -t targets < <(printf '%s\n' "${targets[@]}" | awk '!seen[$0]++')
|
||||
|
||||
# rm's exit code is not the verdict — the absence assert below is (a
|
||||
# half-removed tree must be reported as INCOMPLETE by name, not as a crash).
|
||||
for p in "${targets[@]}"; do rm -rf "$p" || true; done
|
||||
|
||||
# END WITH THE ABSENCE ASSERT: every path re-checked — file, dir OR symlink.
|
||||
# A leftover makes this exit 1 by name; "uninstalled" is a claim, and claims
|
||||
# get verified (the revoke --purge discipline).
|
||||
for p in "${targets[@]}"; do
|
||||
if [ -e "$p" ] || [ -L "$p" ]; then leftover="$leftover $p"; fi
|
||||
done
|
||||
if [ -n "$leftover" ]; then
|
||||
echo "box: uninstall INCOMPLETE — still present:$leftover" >&2
|
||||
echo "box: remove them by hand, and re-check each path is really gone." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "box: uninstalled — removed:"
|
||||
for p in "${targets[@]}"; do echo "box: · $p"; done
|
||||
if [ "$purge_host" -eq 0 ]; then
|
||||
echo "box: note — the host stack (boxnet, firewall), if this host has one, was NOT touched:"
|
||||
echo "box: run teardown-host from a checkout (host/teardown-host.sh), or use --purge-host next time."
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_help() { show_help "${args[0]:-}"; }
|
||||
|
||||
# The escape hatch. The box is resolved and tag-checked; everything else is
|
||||
|
|
|
|||
|
|
@ -202,10 +202,11 @@ EOF
|
|||
export BOX_SKIP_SETUP_HOST=1
|
||||
fi
|
||||
|
||||
# The installer is a no-op when box is already installed — upgrading is
|
||||
# uninstall-then-install, by design. The drill re-proves a tree from scratch
|
||||
# every run, so it does the uninstall itself: clear any prior tree and symlink
|
||||
# before installing, or install.sh would correctly refuse to touch them.
|
||||
# The installer converges when a version is already installed (0.7.0's
|
||||
# versioned layout: re-running the same version is a no-op, and an upgrade
|
||||
# lands side-by-side without flipping under boxes). The drill re-proves a
|
||||
# tree from SCRATCH every run — a fresh host, not a converged one — so it
|
||||
# removes the whole install root and symlink first.
|
||||
rm -rf "$HOME/.local/share/box" "$HOME/.local/bin/box"
|
||||
|
||||
# The installer prompts (install? set up host?) and reads /dev/tty. The drill
|
||||
|
|
@ -224,7 +225,7 @@ EOF
|
|||
# reads BOX_* — the vars were ignored, main was installed, and the run drilled
|
||||
# the wrong tree while reporting success. A drill that silently drills the
|
||||
# wrong code is worse than one that fails.
|
||||
got="$(cat "$HOME/.local/share/box/INSTALLED_FROM" 2>/dev/null || echo '<unknown>')"
|
||||
got="$(cat "$HOME/.local/share/box/current/INSTALLED_FROM" 2>/dev/null || echo '<unknown>')"
|
||||
if [ "$got" != "$REPO@$REF" ]; then
|
||||
echo "drill: FATAL — asked to install $REPO@$REF, but the tree says '$got'." >&2
|
||||
echo " Your local drill.sh is probably STALE (pre-0.5.0 it passed CLAUDEBOX_*," >&2
|
||||
|
|
@ -302,7 +303,7 @@ if [ "${DRILL_OWNS_SETUP:-0}" != 1 ]; then
|
|||
echo " install.sh is supposed to run the host setup itself (#64), and setup-host" >&2
|
||||
echo " is supposed to converge in one run (#63). One of those did not happen." >&2
|
||||
echo " reproduce with the output visible:" >&2
|
||||
echo " ~/.local/share/box/host/setup-host.sh" >&2
|
||||
echo " ~/.local/share/box/current/host/setup-host.sh" >&2
|
||||
echo " or hand setup back to the drill: DRILL_OWNS_SETUP=1 $SELF" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -361,7 +362,7 @@ left="$(incus list --format csv --columns n 2>/dev/null | tr '\n' ' ')"
|
|||
# proves is idempotency: a second run over a cleaned host is a no-op that
|
||||
# restores the stack rather than a fresh build.
|
||||
inf "running setup-host.sh (post-clean convergence: restores dns.mode and any reverted mutations)…"
|
||||
if ! timeout -k 10 300 ~/.local/share/box/host/setup-host.sh; then
|
||||
if ! timeout -k 10 300 ~/.local/share/box/current/host/setup-host.sh; then
|
||||
echo "drill: setup-host.sh failed or timed out (>5 min)." >&2
|
||||
echo " it should take seconds on a host that already has incus. usual causes:" >&2
|
||||
echo " · instances still attached to boxnet while its ACLs are reconfigured" >&2
|
||||
|
|
@ -469,7 +470,7 @@ phase "B. The box surface"
|
|||
# ===========================================================================
|
||||
# Compare against the installed tree's VERSION file, not a hardcoded number —
|
||||
# a pinned literal here would fail the drill on every release.
|
||||
expected="$(cat "$HOME/.local/share/box/VERSION" 2>/dev/null || echo '?')"
|
||||
expected="$(cat "$HOME/.local/share/box/current/VERSION" 2>/dev/null || echo '?')"
|
||||
v="$(box --version 2>&1)"
|
||||
case "$v" in *"$expected"*) ok "box --version → $v" ;; *) no "version mismatch: CLI says '$v', VERSION file says '$expected'" ;; esac
|
||||
|
||||
|
|
@ -498,7 +499,7 @@ box new --name tpl --template nosuch 2>&1 | grep -q 'no such template' \
|
|||
# The one rule that keeps templates honest: no key can name a network. Plant a
|
||||
# bad template in the installed tree (the drill owns this host), expect the
|
||||
# parser to reject it BY NAME, remove it.
|
||||
badt="$HOME/.local/share/box/templates/cbdrill-bad"
|
||||
badt="$HOME/.local/share/box/current/templates/cbdrill-bad"
|
||||
mkdir -p "$badt" && printf 'BOX_IMAGE="x"\nBOX_USER="y"\nBOX_NETWORK="lan"\n' >"$badt/box.env" && : >"$badt/user-data.yaml"
|
||||
box new --name tpl --template cbdrill-bad 2>&1 | grep -q "unknown key 'BOX_NETWORK'" \
|
||||
&& ok "a template cannot name a network — BOX_NETWORK rejected by name" \
|
||||
|
|
@ -889,7 +890,7 @@ phase "M. Migration — the pre-0.4.0 → box transition (host/migrate-host.sh)"
|
|||
# tag on the OLD network — exactly what a pre-0.4.0 host carries. Then prove
|
||||
# migrate-host.sh moves it onto the new stack with its identity intact, and
|
||||
# retires the legacy stack only once it is empty.
|
||||
MIG="$HOME/.local/share/box/host/migrate-host.sh"
|
||||
MIG="$HOME/.local/share/box/current/host/migrate-host.sh"
|
||||
if [ ! -f "$MIG" ]; then
|
||||
no "migrate-host.sh not installed — cannot drill the transition"
|
||||
else
|
||||
|
|
@ -985,5 +986,5 @@ fi
|
|||
echo
|
||||
inf "this host still has Incus, boxnet, the ACL, the profile and the firewall rules"
|
||||
inf "(plus, unless re-run: dns.mode=none and NIC filtering from the D phase)."
|
||||
inf "to undo: ~/.local/share/box/host/teardown-host.sh [--purge-incus]"
|
||||
inf "to undo: box uninstall --purge-host (or ~/.local/share/box/current/host/teardown-host.sh)"
|
||||
[ "$fail" -eq 0 ]
|
||||
|
|
|
|||
|
|
@ -3,20 +3,34 @@
|
|||
# ancestor created, so one teardown cleans a host of any generation: all boxes
|
||||
# (both tags), the boxnet/claudenet networks + ACLs, the box-net/claude-dev
|
||||
# profiles, and both generations of firewall units and nft tables.
|
||||
# Usage: ./host/teardown-host.sh [--purge-incus]
|
||||
# Usage: ./host/teardown-host.sh [--purge-incus] [--yes]
|
||||
# --purge-incus also apt-purge Incus itself (skipped if non-box
|
||||
# instances still exist on this host)
|
||||
# --yes skip the confirmation (BOX_YES=1 does the same) — for
|
||||
# automation: CI's uninstall drill and 'box uninstall
|
||||
# --purge-host' run this unattended
|
||||
set -euo pipefail
|
||||
|
||||
purge=false
|
||||
[ "${1:-}" = "--purge-incus" ] && purge=true
|
||||
purge=false; yes=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--purge-incus) purge=true ;;
|
||||
--yes|-y) yes=1 ;;
|
||||
*) echo "teardown-host: unknown option: $arg" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[ -n "${BOX_YES:-}" ] && yes=1
|
||||
|
||||
echo "This removes ALL boxes (uncommitted work in them is lost), the"
|
||||
echo "boxnet/claudenet networks, ACLs, profiles, and the box firewall rules"
|
||||
echo "(both current and pre-0.4.0 names)."
|
||||
$purge && echo "Incus itself will also be uninstalled (--purge-incus)."
|
||||
read -rp "Continue? [y/N] " a
|
||||
case "$a" in y|Y) ;; *) echo "aborted"; exit 1 ;; esac
|
||||
if [ "$yes" -eq 1 ]; then
|
||||
echo "(confirmed non-interactively: --yes/BOX_YES)"
|
||||
else
|
||||
read -rp "Continue? [y/N] " a
|
||||
case "$a" in y|Y) ;; *) echo "aborted"; exit 1 ;; esac
|
||||
fi
|
||||
|
||||
# Instances — both tag generations, one delete at a time (a multi-name
|
||||
# 'incus delete' aborts at the first missing name).
|
||||
|
|
@ -72,4 +86,4 @@ if $purge; then
|
|||
fi
|
||||
fi
|
||||
|
||||
echo "Teardown complete. (Your ~/.local/bin/box symlink and ~/.local/share/box remain — remove by hand if wanted.)"
|
||||
echo "Teardown complete. (The box install tree itself remains — 'box uninstall' removes it, with a zero-residue check.)"
|
||||
|
|
|
|||
Loading…
Reference in a new issue