Restart NotReady Pimox worker during heal

This commit is contained in:
juvdiaz 2026-06-30 14:10:27 -06:00
parent 91df63af96
commit 8e19df1c71
7 changed files with 63 additions and 9 deletions

View File

@ -284,7 +284,9 @@ Normal `status` remains read-only. The heal planner maps known findings to
guarded actions, chooses one highest-impact auto-eligible finding, checks the guarded actions, chooses one highest-impact auto-eligible finding, checks the
proposed command against `infra/agent-sandbox/policy.tsv`, and runs only that proposed command against `infra/agent-sandbox/policy.tsv`, and runs only that
one action. Re-run `./jeannie status` after every heal cycle so Jeannie one action. Re-run `./jeannie status` after every heal cycle so Jeannie
can reassess before touching the next issue. Destructive actions remain blocked. can reassess before touching the next issue. If a specific Pimox worker is
NotReady, heal targets that worker with `./jeannie workers restart N`
instead of repeating a broad cluster start. Destructive actions remain blocked.
## Model Behavior Observatory ## Model Behavior Observatory

View File

@ -284,7 +284,9 @@ Normal `status` remains read-only. The heal planner maps known findings to
guarded actions, chooses one highest-impact auto-eligible finding, checks the guarded actions, chooses one highest-impact auto-eligible finding, checks the
proposed command against `infra/agent-sandbox/policy.tsv`, and runs only that proposed command against `infra/agent-sandbox/policy.tsv`, and runs only that
one action. Re-run `./{{ main_script }} status` after every heal cycle so Jeannie one action. Re-run `./{{ main_script }} status` after every heal cycle so Jeannie
can reassess before touching the next issue. Destructive actions remain blocked. can reassess before touching the next issue. If a specific Pimox worker is
NotReady, heal targets that worker with `./{{ main_script }} workers restart N`
instead of repeating a broad cluster start. Destructive actions remain blocked.
## Model Behavior Observatory ## Model Behavior Observatory

View File

@ -212,7 +212,7 @@ visible Uptime Kuma monitor coverage.
`workers <command>` `workers <command>`
: Manage Kubernetes/Pimox worker lifecycle operations. Commands include `list`, : Manage Kubernetes/Pimox worker lifecycle operations. Commands include `list`,
`tailnet`, `start`, `stop`, `drain`, `uncordon`, `recreate-plan`, and `tailnet`, `start`, `stop`, `restart`, `drain`, `uncordon`, `recreate-plan`, and
`rebalance`. `rebalance`.
`change-journal {list|path}` `change-journal {list|path}`

View File

@ -8,6 +8,7 @@ pattern decision reason
./jeannie ai-evals allow Read-only eval harness. ./jeannie ai-evals allow Read-only eval harness.
./jeannie prompt-injection-lab allow Read-only security lab harness. ./jeannie prompt-injection-lab allow Read-only security lab harness.
./jeannie start-cluster allow Idempotent recovery path that starts saved Kubernetes services and worker VMs without destroying state. ./jeannie start-cluster allow Idempotent recovery path that starts saved Kubernetes services and worker VMs without destroying state.
./jeannie workers restart allow Targeted Pimox worker VM restart for one-at-a-time self-healing.
./jeannie deploy-gitea allow Idempotent Debian-hosted Gitea Compose repair path for this homelab. ./jeannie deploy-gitea allow Idempotent Debian-hosted Gitea Compose repair path for this homelab.
./jeannie rpi-services allow Idempotent RPi Pi-hole/Unbound/Uptime Kuma Compose repair path for this homelab. ./jeannie rpi-services allow Idempotent RPi Pi-hole/Unbound/Uptime Kuma Compose repair path for this homelab.
./jeannie up approval High-blast-radius deployment. ./jeannie up approval High-blast-radius deployment.

1 pattern decision reason
8 ./jeannie ai-evals allow Read-only eval harness.
9 ./jeannie prompt-injection-lab allow Read-only security lab harness.
10 ./jeannie start-cluster allow Idempotent recovery path that starts saved Kubernetes services and worker VMs without destroying state.
11 ./jeannie workers restart allow Targeted Pimox worker VM restart for one-at-a-time self-healing.
12 ./jeannie deploy-gitea allow Idempotent Debian-hosted Gitea Compose repair path for this homelab.
13 ./jeannie rpi-services allow Idempotent RPi Pi-hole/Unbound/Uptime Kuma Compose repair path for this homelab.
14 ./jeannie up approval High-blast-radius deployment.

View File

@ -6579,7 +6579,7 @@ Cluster Lifecycle
rebuild-cluster Recreate the cluster through the guarded path. rebuild-cluster Recreate the cluster through the guarded path.
stop-cluster Stop Kubernetes and worker VMs without destroy. stop-cluster Stop Kubernetes and worker VMs without destroy.
start-cluster Start Kubernetes and desired worker VMs. start-cluster Start Kubernetes and desired worker VMs.
workers <list|tailnet|start|stop|drain|uncordon|recreate-plan|rebalance> workers <list|tailnet|start|stop|restart|drain|uncordon|recreate-plan|rebalance>
Manage Pimox/Kubernetes workers. Manage Pimox/Kubernetes workers.
move-prometheus-stack-workers Move monitoring workloads to worker nodes. move-prometheus-stack-workers Move monitoring workloads to worker nodes.
doctor-versions [--all|--details|--verbose|--json] doctor-versions [--all|--details|--verbose|--json]

View File

@ -6,6 +6,7 @@ from __future__ import annotations
import argparse import argparse
import json import json
import os import os
import re
import subprocess import subprocess
import sys import sys
from dataclasses import dataclass from dataclasses import dataclass
@ -79,6 +80,26 @@ def rule_for(row: dict[str, str]) -> HealRule | None:
return None return None
def pimox_worker_index(row: dict[str, str]) -> int | None:
text = " ".join(str(row.get(key, "")) for key in ("summary", "detail", "check"))
match = re.search(r"\bpimox-worker-(\d{1,2})\b", text)
if not match:
return None
return int(match.group(1))
def command_for(row: dict[str, str], rule: HealRule) -> tuple[str, str, int]:
if rule.check == "Kubernetes nodes Ready":
index = pimox_worker_index(row)
if index is not None:
return (
f"./jeannie workers restart {index}",
f"Restart Pimox VM for pimox-worker-{index:02d}; a running VM with a NotReady kubelet can be wedged.",
98,
)
return rule.command, rule.reason, rule.priority
def build_plan(status: dict[str, object]) -> list[dict[str, object]]: def build_plan(status: dict[str, object]) -> list[dict[str, object]]:
planned: list[dict[str, object]] = [] planned: list[dict[str, object]] = []
seen_commands: set[str] = set() seen_commands: set[str] = set()
@ -99,19 +120,20 @@ def build_plan(status: dict[str, object]) -> list[dict[str, object]]:
} }
) )
continue continue
if rule.command in seen_commands and rule.auto: command, reason, priority = command_for(row, rule)
if command in seen_commands and rule.auto:
continue continue
seen_commands.add(rule.command) seen_commands.add(command)
planned.append( planned.append(
{ {
"check": row.get("check", ""), "check": row.get("check", ""),
"area": row.get("area", ""), "area": row.get("area", ""),
"status": row.get("status", ""), "status": row.get("status", ""),
"command": rule.command, "command": command,
"risk": rule.risk, "risk": rule.risk,
"auto": rule.auto, "auto": rule.auto,
"priority": rule.priority, "priority": priority,
"reason": rule.reason, "reason": reason,
"summary": row.get("summary", ""), "summary": row.get("summary", ""),
} }
) )

View File

@ -21,6 +21,7 @@ Commands:
tailnet Install/join Tailscale on generated Pimox workers tailnet Install/join Tailscale on generated Pimox workers
start [index|all] Start Pimox worker VM(s) start [index|all] Start Pimox worker VM(s)
stop [index|all] Gracefully stop Pimox worker VM(s) stop [index|all] Gracefully stop Pimox worker VM(s)
restart [index|all] Gracefully restart Pimox worker VM(s)
drain <node> Drain a Kubernetes worker node drain <node> Drain a Kubernetes worker node
uncordon <node> Mark a Kubernetes worker node schedulable uncordon <node> Mark a Kubernetes worker node schedulable
recreate-plan <index> Print the safe recreate sequence for one Pimox worker recreate-plan <index> Print the safe recreate sequence for one Pimox worker
@ -138,6 +139,29 @@ stop_workers() {
done done
} }
restart_workers() {
local index
local vmid
local timeout="${LAB_WORKER_SHUTDOWN_TIMEOUT:-90}"
for index in $(target_indexes "${1:-all}"); do
vmid="$(vmid_for_index "$index")"
echo "Restarting Pimox worker index $index VM $vmid..."
pimox_ssh "set -eu
if ! sudo '$PIMOX_QM_BIN' status '$vmid' >/dev/null 2>&1; then
echo 'Pimox worker VM $vmid does not exist.' >&2
exit 1
fi
if sudo '$PIMOX_QM_BIN' status '$vmid' | grep -q 'status: running'; then
if ! sudo '$PIMOX_QM_BIN' shutdown '$vmid' --timeout '$timeout'; then
echo 'Graceful shutdown timed out; forcing stop for Pimox worker VM $vmid.'
sudo '$PIMOX_QM_BIN' stop '$vmid'
fi
fi
sudo '$PIMOX_QM_BIN' start '$vmid'"
done
}
drain_node() { drain_node() {
local node="${1:-}" local node="${1:-}"
@ -277,6 +301,9 @@ case "${1:-}" in
stop) stop)
stop_workers "${2:-all}" stop_workers "${2:-all}"
;; ;;
restart)
restart_workers "${2:-all}"
;;
drain) drain)
drain_node "${2:-}" drain_node "${2:-}"
;; ;;