#!/usr/bin/env bash # # ka-status — per-host kernel-agent state summary. # # Reads fleet/*.yaml manifests + queries Gitea for open [ka:*] issues + # probes each host (where reachable) for the installed kernel-package # version. Designed to give a first-look "what's the state" before any # ka-promote / ka-install action. # # Usage: # ka-status # summary across all manifests # ka-status # detail for one host # # Read-only. Never mutates state. No sudo. No SSH-into-host writes. # # Phase 1 deliverable. Future ka-* CLI verbs (ka-promote / ka-close / # ka-install) build on the same Gitea-API + manifest-parsing skeleton. set -euo pipefail GITEA_URL="${GITEA_URL:-https://git.reauktion.de}" REPO="${KERNEL_AGENT_REPO:-marfrit/kernel-agent}" TOKEN_FILE="${KERNEL_AGENT_TOKEN_FILE:-/opt/herding/etc/claude-identities/noether.creds}" # Resolve token from per-host claude-identity creds, or env override. token="" if [ -n "${GITEA_TOKEN:-}" ]; then token="$GITEA_TOKEN" elif [ -r "$TOKEN_FILE" ]; then token="$(grep -E '^GITEA_TOKEN=' "$TOKEN_FILE" | head -1 | cut -d= -f2)" fi # Locate fleet/ — script lives in bin/ next to fleet/. script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" fleet_dir="${script_dir}/../fleet" [ -d "$fleet_dir" ] || { echo "fleet/ not found relative to $script_dir" >&2; exit 2; } api_get() { local path="$1" local args=(--silent --show-error --max-time 15) [ -n "$token" ] && args+=(-H "Authorization: token $token") curl "${args[@]}" "${GITEA_URL}/api/v1/${path}" } # Open ka-prefixed issues, JSON array on stdout. fetch_ka_issues() { api_get "repos/${REPO}/issues?state=open&type=issues&limit=50" \ | python3 -c ' import json, sys issues = json.load(sys.stdin) def kind(t): if not t.startswith("["): return None p = t.find("]") return t[1:p] if p > 0 else None out = [{ "number": i["number"], "title": i["title"], "kind": kind(i["title"]), } for i in issues if kind(i["title"]) and kind(i["title"]).startswith("ka:")] print(json.dumps(out)) ' 2>/dev/null || echo "[]" } # Parse a fleet/.yaml manifest (very-narrow YAML subset). manifest_field() { local file="$1" key="$2" grep -E "^[[:space:]]*${key}:" "$file" | head -1 | sed -E "s/^[^:]*:[[:space:]]*//; s/[[:space:]]+#.*//; s/^[\"']//; s/[\"']$//" } manifest_pkgname() { local file="$1" awk '/^package:/{p=1; next} p && /^[a-z]/{p=0} p && /^[[:space:]]+name:/{sub(/^[[:space:]]+name:[[:space:]]*/,""); print; exit}' "$file" } # Probe a host for installed kernel-package version (best-effort, non-blocking). probe_installed() { local host="$1" pkg="$2" ssh -o ConnectTimeout=3 -o BatchMode=yes -o StrictHostKeyChecking=accept-new \ "${host}.fritz.box" "pacman -Q '$pkg' 2>/dev/null || dpkg-query -W -f='\${Package} \${Version}\n' '$pkg' 2>/dev/null || echo 'host-up:not-installed'" 2>/dev/null \ || echo "host-down" } issues_json="$(fetch_ka_issues)" # Per-host issue grouping — match on title containing the host name (cheap heuristic; # proper kernel-agent will tag issues with a host label). issues_for_host() { local host="$1" echo "$issues_json" | python3 -c " import json, sys host = '$host' issues = json.load(sys.stdin) hits = [i for i in issues if host in i['title'].lower()] for h in hits: print(f\" #{h['number']} [{h['kind']}] {h['title']}\") " 2>/dev/null } print_host() { local file="$1" local host pkg host="$(basename "$file" .yaml)" pkg="$(manifest_pkgname "$file")" local arch="$(manifest_field "$file" arch)" local soc="$(manifest_field "$file" soc)" local board="$(manifest_field "$file" board)" printf '\n══ %s ══\n' "$host" printf ' manifest: arch=%s soc=%s board=%s\n' "$arch" "$soc" "$board" printf ' package: %s\n' "$pkg" if [ -n "$pkg" ]; then printf ' installed: %s\n' "$(probe_installed "$host" "$pkg")" fi local n=0 while IFS= read -r line; do [ -z "$line" ] && continue if [ $n -eq 0 ]; then printf ' open ka-issues:\n'; fi printf '%s\n' "$line" n=$((n+1)) done < <(issues_for_host "$host") [ $n -eq 0 ] && printf ' open ka-issues: (none for this host)\n' } if [ $# -ge 1 ]; then f="${fleet_dir}/${1}.yaml" [ -r "$f" ] || { echo "no manifest for host '$1'" >&2; exit 2; } print_host "$f" else printf 'kernel-agent status (repo: %s)\n' "$REPO" printf 'open [ka:*] issues total: %s\n' "$(echo "$issues_json" | python3 -c 'import json,sys; print(len(json.load(sys.stdin)))')" for f in "$fleet_dir"/*.yaml; do [ -e "$f" ] || continue print_host "$f" done fi