feat: 'claudebox doctor' — the host-health checks as a first-class verb #50
3 changed files with 41 additions and 5 deletions
|
|
@ -89,6 +89,7 @@ claudebox down <box> # stop (state kept; `start` resumes)
|
||||||
claudebox start <box> # start a stopped box
|
claudebox start <box> # start a stopped box
|
||||||
claudebox rm <box> [--force] # delete the box + its snapshots (asks first)
|
claudebox rm <box> [--force] # delete the box + its snapshots (asks first)
|
||||||
claudebox incus <box> -- <args...> # escape hatch: any incus command, box resolved
|
claudebox incus <box> -- <args...> # escape hatch: any incus command, box resolved
|
||||||
|
claudebox doctor [--fix|--pin-dns] # is this host fit to mint boxes? diagnose from ground truth
|
||||||
claudebox status # deprecated alias for `list`
|
claudebox status # deprecated alias for `list`
|
||||||
claudebox help [<command>] # full help, or one command's page
|
claudebox help [<command>] # full help, or one command's page
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ CMDS=(
|
||||||
"start^<box>^box^Start a stopped box^incus:start^started {}"
|
"start^<box>^box^Start a stopped box^incus:start^started {}"
|
||||||
"rm^<box> [--force]^box,confirm^Delete a box and its snapshots — irreversible, and it asks first^incus:delete -f^removed {}"
|
"rm^<box> [--force]^box,confirm^Delete a box and its snapshots — irreversible, and it asks first^incus:delete -f^removed {}"
|
||||||
"incus^<box> -- <args...>^box^Escape hatch: run any incus command against a box^fn:cmd_incus^"
|
"incus^<box> -- <args...>^box^Escape hatch: run any incus command against a box^fn:cmd_incus^"
|
||||||
|
"doctor^[--fix | --pin-dns]^^Is this host fit to mint boxes? Diagnose the daemon, network, DNS, isolation^fn:cmd_doctor^"
|
||||||
"status^^^Deprecated alias for 'list'^fn:cmd_status^"
|
"status^^^Deprecated alias for 'list'^fn:cmd_status^"
|
||||||
"help^[<command>]^^This help, or 'claudebox help <command>' for one command^fn:cmd_help^"
|
"help^[<command>]^^This help, or 'claudebox help <command>' for one command^fn:cmd_help^"
|
||||||
)
|
)
|
||||||
|
|
@ -282,6 +283,25 @@ Changing the profile, the network, a device or a security.* key can take a box
|
||||||
outside the isolation stack. claudebox warns and then does as you asked — from
|
outside the isolation stack. claudebox warns and then does as you asked — from
|
||||||
there, the trust boundary is yours to keep.
|
there, the trust boundary is yours to keep.
|
||||||
EOF
|
EOF
|
||||||
|
;;
|
||||||
|
doctor) cat <<'EOF'
|
||||||
|
Answer "is this host fit to mint boxes?" from ground truth, not config claims:
|
||||||
|
is the Incus daemon answering, is a dnsmasq actually serving claudenet, does
|
||||||
|
the kernel's bridge port say 'isolated on', is the resolver pinned or is a
|
||||||
|
host VPN's DNS leaking into boxes, can a box actually resolve names. Every
|
||||||
|
check exists because its fault has happened — most kill a cold mint with a
|
||||||
|
cloud-init error that names none of them.
|
||||||
|
|
||||||
|
--fix also revert what a drill run may have left behind
|
||||||
|
--pin-dns pin claudenet's resolver to public upstreams and re-test
|
||||||
|
(setup-host.sh now pins by default; this is the quick test)
|
||||||
|
|
||||||
|
claudebox doctor
|
||||||
|
claudebox doctor --fix
|
||||||
|
|
||||||
|
Exit 0 = clean; 1 = problems found (each printed with its fix). Read-only
|
||||||
|
unless --fix or --pin-dns is given.
|
||||||
|
EOF
|
||||||
;;
|
;;
|
||||||
status) cat <<'EOF'
|
status) cat <<'EOF'
|
||||||
Deprecated alias for 'claudebox list'. It ignored the <box> argument it
|
Deprecated alias for 'claudebox list'. It ignored the <box> argument it
|
||||||
|
|
@ -330,6 +350,8 @@ while [ $# -gt 0 ]; do
|
||||||
# An unrecognized flag used to be swallowed as a positional — so a typo'd
|
# An unrecognized flag used to be swallowed as a positional — so a typo'd
|
||||||
# --labl silently became a snapshot's label. Say so instead.
|
# --labl silently became a snapshot's label. Say so instead.
|
||||||
-*)
|
-*)
|
||||||
|
# doctor's flags belong to the doctor script, not to claudebox
|
||||||
|
if [ "$cmd" = doctor ]; then args+=("$1"); shift; continue; fi
|
||||||
if [ "$cmd" = exec ] || [ "$cmd" = incus ]; then
|
if [ "$cmd" = exec ] || [ "$cmd" = incus ]; then
|
||||||
usage_error "unknown option: $1 — a command's own flags go after --, as in '$(synopsis_of "$cmd")'"
|
usage_error "unknown option: $1 — a command's own flags go after --, as in '$(synopsis_of "$cmd")'"
|
||||||
fi
|
fi
|
||||||
|
|
@ -473,8 +495,8 @@ cmd_new() {
|
||||||
echo "claudebox: '$name' is still up — inspect it, then delete it:" >&2
|
echo "claudebox: '$name' is still up — inspect it, then delete it:" >&2
|
||||||
echo " claudebox incus $name -- exec {} -- tail -50 /var/log/cloud-init-output.log" >&2
|
echo " claudebox incus $name -- exec {} -- tail -50 /var/log/cloud-init-output.log" >&2
|
||||||
echo " claudebox rm $name" >&2
|
echo " claudebox rm $name" >&2
|
||||||
echo "claudebox: a box that cannot resolve DNS is usually the HOST's resolver" >&2
|
echo "claudebox: a failed mint is usually the HOST's fault (a wedged daemon, a dnsmasq" >&2
|
||||||
echo " (a VPN/Tailscale resolver the box inherits — see issue #33)." >&2
|
echo " not serving, a VPN resolver the box inherits). Diagnose it: claudebox doctor" >&2
|
||||||
die "cloud-init failed — the box is incomplete, so refusing to hand it over"
|
die "cloud-init failed — the box is incomplete, so refusing to hand it over"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
@ -583,6 +605,18 @@ cmd_status() {
|
||||||
list_all
|
list_all
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The host-health checks live in drill/doctor.sh — grown by the drill, but
|
||||||
|
# every fault they diagnose (a wedged daemon, a dnsmasq that isn't serving,
|
||||||
|
# a VPN resolver boxes inherit, isolation off in the kernel) is a USER's
|
||||||
|
# fault first: each one has killed a cold mint or silently weakened a
|
||||||
|
# boundary. The install tree ships the whole repo, so delegate — one
|
||||||
|
# hardened script, two audiences.
|
||||||
|
cmd_doctor() {
|
||||||
|
local script="$root/drill/doctor.sh"
|
||||||
|
[ -f "$script" ] || die "doctor script not found at $script — re-run install.sh"
|
||||||
|
exec bash "$script" "${args[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
cmd_help() { show_help "${args[0]:-}"; }
|
cmd_help() { show_help "${args[0]:-}"; }
|
||||||
|
|
||||||
# The escape hatch. The box is resolved and tag-checked; everything else is
|
# The escape hatch. The box is resolved and tag-checked; everything else is
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# doctor.sh — is this host in a fit state to drill, and if not, what is wrong?
|
# doctor.sh — is this host fit to mint boxes (and to drill), and if not, what
|
||||||
|
# is wrong? Users reach it as 'claudebox doctor'; the drill runs it directly.
|
||||||
#
|
#
|
||||||
# bash drill/doctor.sh # report
|
# bash drill/doctor.sh # report
|
||||||
# bash drill/doctor.sh --fix # report, then revert what the drill left behind
|
# bash drill/doctor.sh --fix # report, then revert what the drill left behind
|
||||||
|
|
@ -243,10 +244,10 @@ fi
|
||||||
|
|
||||||
head_ "Verdict"
|
head_ "Verdict"
|
||||||
if [ "$bad" -eq 0 ]; then
|
if [ "$bad" -eq 0 ]; then
|
||||||
printf ' \033[32mclean\033[0m — this host is fit to drill.\n\n'
|
printf ' \033[32mclean\033[0m — this host is fit to mint boxes (and to drill).\n\n'
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
printf ' \033[31m%s problem(s)\033[0m — this host is NOT fit to drill.\n' "$bad"
|
printf ' \033[31m%s problem(s)\033[0m — this host is NOT fit to mint boxes (or to drill).\n' "$bad"
|
||||||
if [ "$FIX" = 1 ]; then
|
if [ "$FIX" = 1 ]; then
|
||||||
printf ' reverted what could be reverted; re-run doctor to confirm.\n\n'
|
printf ' reverted what could be reverted; re-run doctor to confirm.\n\n'
|
||||||
else
|
else
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue