Merge remote-tracking branch 'fork/feat/migrate-host' into integration/0.5.0
# Conflicts: # drill/drill.sh
This commit is contained in:
commit
05287c96d7
2 changed files with 208 additions and 4 deletions
|
|
@ -159,8 +159,8 @@ This will, ON THIS HOST ($(hostname)):
|
|||
· install Incus and a systemd unit
|
||||
· create a network (boxnet), an ACL, and a profile
|
||||
· rewrite firewall rules (nft or UFW, and Docker's DOCKER-USER chain)
|
||||
· create and destroy instances named: drill, clone, archive, peer, payroll, cbprobe, cbcopy, tpl, codex, grok
|
||||
· mutate the network and profile mid-run to rehearse the #16 hardening
|
||||
· create and destroy instances named: drill, clone, archive, peer, payroll, cbprobe, cbcopy, tpl, codex, grok, legacybox
|
||||
· build a faithful legacy stack (claudenet/10.87, claude-dev) to drill migration
|
||||
Only do this on a machine you can format.
|
||||
EOF
|
||||
[ -t 0 ] || { echo "drill: no TTY to confirm on — pass --yes if you mean it." >&2; exit 2; }
|
||||
|
|
@ -243,7 +243,7 @@ fi
|
|||
inf "clearing anything a previous run left behind…"
|
||||
# One name at a time — 'incus delete -f a b c' aborts at the first MISSING name,
|
||||
# which is how run 2 inherited run 1's boxes and cascaded five false FAILs.
|
||||
for n in drill clone archive peer payroll cbprobe cbcopy cbnotours tpl codex grok; do
|
||||
for n in drill clone archive peer payroll cbprobe cbcopy cbnotours tpl codex grok legacybox; do
|
||||
timeout -k 5 60 incus delete -f "$n" >/dev/null 2>&1
|
||||
done
|
||||
if incus network show boxnet >/dev/null 2>&1; then
|
||||
|
|
@ -697,6 +697,73 @@ if [ "$BASELINE_OK" -ne 1 ]; then
|
|||
inf "start with: bash drill/doctor.sh"
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
phase "M. Migration — the pre-0.4.0 → box transition (host/migrate-host.sh)"
|
||||
# ===========================================================================
|
||||
# A fresh host has no legacy stack, so build a faithful one: claudenet on the
|
||||
# OLD subnet, a claude-dev profile pinned to it, and a box tagged with the OLD
|
||||
# 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/claudebox/host/migrate-host.sh"
|
||||
if [ ! -f "$MIG" ]; then
|
||||
no "migrate-host.sh not installed — cannot drill the transition"
|
||||
else
|
||||
inf "building a faithful legacy stack (claudenet/10.87 + claude-dev)…"
|
||||
incus network show claudenet >/dev/null 2>&1 || incus network create claudenet \
|
||||
ipv4.address=10.87.0.1/24 ipv4.nat=true ipv6.address=none >/dev/null 2>&1
|
||||
if ! incus profile show claude-dev >/dev/null 2>&1; then
|
||||
incus profile create claude-dev >/dev/null 2>&1
|
||||
incus profile device add claude-dev root disk pool=default path=/ >/dev/null 2>&1
|
||||
incus profile device add claude-dev eth0 nic network=claudenet name=eth0 \
|
||||
security.port_isolation=true >/dev/null 2>&1
|
||||
fi
|
||||
# A minimal legacy box: no template payload, just boots and networks on the
|
||||
# old stack, wearing the old tag. This is what migrate has to move.
|
||||
printf '\n minting a faithful legacy box on the old stack…\n'
|
||||
if mint_legacy=$(incus launch images:debian/13/cloud legacybox --profile claude-dev \
|
||||
--config user.claudebox=1 --vm --device root,size=20GiB 2>&1); then
|
||||
wait_box legacybox && ok "legacy box up on the old stack (claudenet, user.claudebox=1)" \
|
||||
|| no "legacy box never came up — cannot drill migration"
|
||||
box list 2>/dev/null | grep -q '^legacybox' \
|
||||
&& ok "box list shows the legacy box (dual-tag matching)" || no "legacy box invisible to 'box list'"
|
||||
|
||||
# Retire must REFUSE while a legacy box exists.
|
||||
bash "$MIG" --retire-legacy 2>&1 | grep -qi 'legacy boxes still exist' \
|
||||
&& ok "retire-legacy refuses while a legacy box remains" \
|
||||
|| no "retire-legacy did NOT refuse with a legacy box present — it would strip an in-use stack"
|
||||
|
||||
# Re-home it.
|
||||
printf ' re-homing the legacy box…\n'
|
||||
bash "$MIG" --box legacybox 2>&1 | sed 's/^/ /'
|
||||
[ "$(incus config get legacybox user.box 2>/dev/null)" = 1 ] \
|
||||
&& ok "migrate: legacy box now tagged user.box=1" || no "migrate: user.box tag not set"
|
||||
[ "$(incus config get legacybox user.box.user 2>/dev/null)" = claude ] \
|
||||
&& ok "migrate: legacy box mapped to the claude user" || no "migrate: user.box.user not claude"
|
||||
incus config show legacybox 2>/dev/null | grep -q '^- box-net' \
|
||||
&& ok "migrate: legacy box reassigned to box-net (the new placement contract)" \
|
||||
|| no "migrate: legacy box is NOT on box-net"
|
||||
lip="$(boxnet_ip legacybox)"
|
||||
[ -n "$lip" ] && ok "migrate: legacy box got a boxnet address ($lip) — network move landed" \
|
||||
|| no "migrate: legacy box has no 10.88 address — the move did not take"
|
||||
in_box legacybox getent hosts deb.debian.org >/dev/null 2>&1 \
|
||||
&& ok "migrate: re-homed box resolves + reaches the internet on its new leg" \
|
||||
|| no "migrate: re-homed box cannot resolve on boxnet"
|
||||
|
||||
# No legacy boxes remain → retire must now SUCCEED and leave nothing.
|
||||
printf ' retiring the (now empty) legacy stack…\n'
|
||||
bash "$MIG" --retire-legacy 2>&1 | sed 's/^/ /'
|
||||
incus network show claudenet >/dev/null 2>&1 \
|
||||
&& no "retire-legacy left claudenet behind" || ok "retire-legacy removed claudenet"
|
||||
incus profile show claude-dev >/dev/null 2>&1 \
|
||||
&& no "retire-legacy left claude-dev behind" || ok "retire-legacy removed claude-dev"
|
||||
|
||||
box rm legacybox --force >/dev/null 2>&1
|
||||
else
|
||||
no "could not launch the legacy box: $(printf '%s' "$mint_legacy" | tail -1)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
if [ "$KEEP" = 1 ]; then
|
||||
phase "Boxes left up (--keep-boxes)"
|
||||
|
|
@ -704,7 +771,7 @@ if [ "$KEEP" = 1 ]; then
|
|||
inf "note: the D-phase mutations (dns.mode=none, NIC filtering) are still applied"
|
||||
else
|
||||
# every name the drill can have left, whatever branch a partial run took
|
||||
for n in drill clone archive peer tpl codex grok; do box rm "$n" --force >/dev/null 2>&1; done
|
||||
for n in drill clone archive peer tpl codex grok legacybox; do box rm "$n" --force >/dev/null 2>&1; done
|
||||
# Assert OUR boxes are gone — not that the host is empty. The rm loop above
|
||||
# already embodies the discipline (only names the drill minted); demanding
|
||||
# 'no boxes yet' here would flag any pre-existing operator box as a failure.
|
||||
|
|
|
|||
137
host/migrate-host.sh
Normal file
137
host/migrate-host.sh
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
#!/usr/bin/env bash
|
||||
# migrate-host.sh — move a host from the pre-0.4.0 'claudebox' stack to 'box'.
|
||||
#
|
||||
# The zero-ceremony transition is just install.sh + setup-host.sh: that leaves
|
||||
# a DUAL-STACK host where legacy boxes (tag user.claudebox=1, claudenet/10.87,
|
||||
# claude-dev) keep working while new mints land on boxnet/10.88. This script is
|
||||
# the two things that path does not do:
|
||||
#
|
||||
# migrate-host.sh --box <name> re-home ONE legacy box onto the new stack
|
||||
# migrate-host.sh --all-boxes re-home every legacy box
|
||||
# migrate-host.sh --retire-legacy remove the legacy stack (refuses while any
|
||||
# legacy box still exists)
|
||||
#
|
||||
# One action per invocation, idempotent, loud about what it did. Re-homing
|
||||
# PRESERVES the box's authed state (Claude login, git creds — the expensive
|
||||
# thing); it does not re-mint. The order is load-bearing: tag first (additive,
|
||||
# reversible), profile last, and verify the box works on its new leg BEFORE
|
||||
# calling it migrated — a box must never end up tagless or profileless.
|
||||
#
|
||||
# NOT 'set -e' around the per-box work: a box that fails one step is reported
|
||||
# and skipped, not a crash that abandons the rest mid-migration.
|
||||
set -u
|
||||
|
||||
GW_NEW=10.88.0.1
|
||||
say() { printf 'migrate: %s\n' "$*"; }
|
||||
warn() { printf 'migrate: WARNING: %s\n' "$*" >&2; }
|
||||
die() { printf 'migrate: ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
mode=""
|
||||
target=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--box) mode=box; target="${2:-}"; shift 2 || die "--box needs a name" ;;
|
||||
--all-boxes) mode=all; shift ;;
|
||||
--retire-legacy) mode=retire; shift ;;
|
||||
-h|--help) sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) die "unknown argument: $1 (see --help)" ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$mode" ] || die "pick one: --box <name> | --all-boxes | --retire-legacy"
|
||||
command -v incus >/dev/null || die "incus is not installed on this host"
|
||||
|
||||
# The new stack must exist before any box can be re-homed onto it. setup-host
|
||||
# creates it; refuse rather than move a box onto a network that isn't there.
|
||||
require_new_stack() {
|
||||
incus network show boxnet >/dev/null 2>&1 || die "boxnet does not exist — run host/setup-host.sh first"
|
||||
incus profile show box-net >/dev/null 2>&1 || die "box-net profile does not exist — run host/setup-host.sh first"
|
||||
}
|
||||
|
||||
legacy_boxes() { incus list "user.claudebox=1" -f csv -c n 2>/dev/null; }
|
||||
|
||||
# Re-home one box. Legacy boxes are all claude boxes (the only template the old
|
||||
# tool minted), so the new metadata is the claude template's.
|
||||
rehome_one() {
|
||||
local b="$1" st
|
||||
incus config get "$b" user.claudebox >/dev/null 2>&1 || { warn "$b is not a legacy box (no user.claudebox tag) — skipping"; return 1; }
|
||||
if [ "$(incus config get "$b" user.box 2>/dev/null)" = 1 ]; then
|
||||
say "$b already carries user.box=1 — already re-homed, skipping"; return 0
|
||||
fi
|
||||
say "re-homing $b …"
|
||||
|
||||
# 1. TAG FIRST — additive and reversible. A box that stops here is still a
|
||||
# valid legacy box (the old tag is untouched) AND now a new one.
|
||||
incus config set "$b" user.box=1 user.box.template=claude user.box.user=claude \
|
||||
|| { warn "$b: could not set new metadata — left untouched"; return 1; }
|
||||
|
||||
# 2. Stop, reassign the profile (this is the network move), restart. Incus
|
||||
# won't reassign a profile on a running instance's NIC cleanly, and the
|
||||
# box needs a fresh DHCP lease on boxnet anyway.
|
||||
st="$(incus list "$b" -f csv -c s 2>/dev/null | head -1)"
|
||||
case "$st" in RUNNING|Running|running) incus stop "$b" >/dev/null 2>&1 || warn "$b: stop was not clean" ;; esac
|
||||
incus profile assign "$b" box-net \
|
||||
|| { warn "$b: profile assign failed — it still has user.box=1 but is on the OLD network; fix by hand"; return 1; }
|
||||
incus start "$b" >/dev/null 2>&1 || { warn "$b: did not restart — start it by hand"; return 1; }
|
||||
|
||||
# 3. VERIFY THE EFFECT, not the exit codes (the whole repo's lesson). The box
|
||||
# must be on 10.88 and actually resolve+reach the internet on its new leg
|
||||
# before we call it migrated.
|
||||
local i ip
|
||||
ip=""
|
||||
for i in $(seq 1 30); do
|
||||
ip="$(incus exec "$b" -- ip -4 -o addr show scope global </dev/null 2>/dev/null \
|
||||
| awk '{for(i=1;i<NF;i++) if($i=="inet" && $(i+1)~/^10\.88\./){split($(i+1),a,"/"); print a[1]; exit}}')"
|
||||
[ -n "$ip" ] && break
|
||||
sleep 2
|
||||
done
|
||||
[ -n "$ip" ] || { warn "$b: never got a 10.88 address after restart — re-home INCOMPLETE, inspect: incus console $b"; return 1; }
|
||||
if incus exec "$b" -- getent hosts deb.debian.org </dev/null >/dev/null 2>&1; then
|
||||
say "$b re-homed: on boxnet ($ip), resolves + reachable, authed state preserved"
|
||||
return 0
|
||||
fi
|
||||
warn "$b is on boxnet ($ip) but cannot resolve — check the new stack's resolver (box doctor)"
|
||||
return 1
|
||||
}
|
||||
|
||||
case "$mode" in
|
||||
box)
|
||||
[ -n "$target" ] || die "--box needs a name"
|
||||
require_new_stack
|
||||
rehome_one "$target"
|
||||
;;
|
||||
all)
|
||||
require_new_stack
|
||||
boxes="$(legacy_boxes)"
|
||||
[ -n "$boxes" ] || { say "no legacy boxes to re-home"; exit 0; }
|
||||
rc=0
|
||||
for b in $boxes; do rehome_one "$b" || rc=1; done
|
||||
[ "$rc" = 0 ] && say "all legacy boxes re-homed" || warn "some boxes need attention (above)"
|
||||
exit "$rc"
|
||||
;;
|
||||
retire)
|
||||
# Refuse while any legacy box still references the old stack — removing an
|
||||
# in-use profile/network fails anyway, and a half-removed stack is worse
|
||||
# than an intact one.
|
||||
remaining="$(legacy_boxes)"
|
||||
if [ -n "$remaining" ]; then
|
||||
die "legacy boxes still exist: $(echo "$remaining" | tr '\n' ' ')
|
||||
re-home them first (--all-boxes), or delete them, then retire."
|
||||
fi
|
||||
say "no legacy boxes remain — removing the legacy stack"
|
||||
incus profile delete claude-dev >/dev/null 2>&1 && say "deleted profile claude-dev"
|
||||
incus network delete claudenet >/dev/null 2>&1 && say "deleted network claudenet"
|
||||
incus network acl delete claude-isolate >/dev/null 2>&1 && say "deleted ACL claude-isolate"
|
||||
sudo systemctl disable --now claudebox-firewall.service >/dev/null 2>&1 && say "disabled claudebox-firewall.service"
|
||||
sudo rm -f /etc/systemd/system/claudebox-firewall.service /usr/local/sbin/claudebox-firewall
|
||||
sudo systemctl daemon-reload
|
||||
sudo nft delete table inet claudebox >/dev/null 2>&1 && say "deleted nft table inet claudebox"
|
||||
sudo nft delete table bridge claudebox >/dev/null 2>&1 && say "deleted nft table bridge claudebox"
|
||||
# Assert the absence — don't trust the removals' exit codes.
|
||||
left=""
|
||||
incus network show claudenet >/dev/null 2>&1 && left="$left claudenet"
|
||||
incus profile show claude-dev >/dev/null 2>&1 && left="$left claude-dev"
|
||||
sudo nft list table bridge claudebox >/dev/null 2>&1 && left="$left nft-bridge"
|
||||
[ -z "$left" ] && say "legacy stack retired — this host is now single-stack (box only)" \
|
||||
|| die "legacy stack NOT fully removed:$left"
|
||||
;;
|
||||
esac
|
||||
Loading…
Reference in a new issue