#!/usr/bin/env bash # The remote box — the other half of the inventory. # # Usage: # ./host.sh status|ports|services # # Announces what it would run over `ssh $HOST` and does not run it. Always the # ssh alias, never a hostname: there is no `Host mcrn.ar` block, so a bare # hostname offers every agent key and trips MaxAuthTries. set -euo pipefail cd "$(dirname "$0")" source ./lib/config.sh source ./lib/estate.sh load_config guard_alias() { if [[ "$HOST" == *.* ]]; then echo "HOST='$HOST' is a hostname, not a ~/.ssh/config alias. Refusing." >&2 exit 1 fi } announce_batch() { echo "would run, over 'ssh $HOST':" printf ' %s\n' "$@" echo echo "read-only, and NOT run. Announce-first applies to EACH batch, not" echo "once per session — so the commands can be read and" echo "learned rather than scrolled past." } guard_alias case "${1:-status}" in status) announce_batch \ "uname -a; uptime; df -h /" \ "docker ps --format '{{.Names}}\t{{.Image}}\t{{.Ports}}'" \ "docker network inspect gateway --format '{{range .Containers}}{{.Name}} {{end}}'" \ "systemctl list-units --type=service --state=running --no-pager" \ "systemctl list-timers --no-pager" echo echo "sudo-only, over 'ssh $HOST_ADMIN' and only where genuinely needed:" echo " wg show # the WireGuard peers that exist nowhere in the tree" ;; ports) announce_batch "ss -ltnp" echo "the estate declares these firewall rules:" estate_get "firewall" | python3 -c ' import json,sys for r in json.load(sys.stdin): print(" %-6s %-5s %s" % (r["port"], r.get("proto","tcp"), r.get("desc","")))' ;; services) announce_batch "docker compose -f ~/ppl/gateway/docker-compose.yml ps" echo "the estate declares $(estate_services "$TARGET" | grep -c . ) service(s) for target '$TARGET'." echo "the gateway compose declares 8. The difference is sibling repos'" echo "stacks joining the shared 'gateway' network — intended design, but" echo "nothing in the tree lists it. The inventory produces that list." ;; *) echo "usage: $0 [status|ports|services]" >&2; exit 1 ;; esac