monitoring: stage.lefrancpaysan like hacktivism (inside+versions via stagepaysan)

Expand default phases for TESTPAYSAN to urls + inside + versions + e2e.
Inside/version/load use low-priv francpaysan-stage-user (stagepaysan)
and stage-lfp-* containers on 9030–9032; public stats.json stays outside-in.
This commit is contained in:
Hernâni Marques 2026-07-17 19:17:40 +02:00
parent 19bbcc09da
commit 08c55e71a7
No known key found for this signature in database
7 changed files with 398 additions and 48 deletions

View file

@ -1,5 +1,9 @@
#!/usr/bin/env bash
# Inside status for bank / exchange / merchant. Hard-capped SSH — never hang forever.
# Inside status for bank / exchange / merchant containers.
# Profiles:
# koopa (default LOCAL_STACK=1) — SSH KOOPA_SSH, containers taler-hacktivism*
# stage-lfp (TESTPAYSAN) — SSH INSIDE_SSH (francpaysan-stage-user / stagepaysan),
# low-priv podman only; ports 90309032; stats remain outside-in (urls).
set -euo pipefail
ROOT=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=lib.sh
@ -7,14 +11,250 @@ source "$ROOT/lib.sh"
# shellcheck source=metrics.sh
source "$ROOT/metrics.sh"
# Area inside.* — container / process state on koopa (SSH)
# Groups: inside.ssh / inside.bank / inside.exchange / inside.merchant / inside.caddy
set_area inside
set_group ssh
PROFILE="${INSIDE_PROFILE:-}"
if [ -z "$PROFILE" ]; then
if [ "${LOCAL_STACK:-0}" = "1" ]; then
PROFILE=koopa
elif [ "${EXPECT_CURRENCY:-}" = "TESTPAYSAN" ] || [ -n "${INSIDE_SSH:-}" ]; then
PROFILE=stage-lfp
else
PROFILE=koopa
fi
fi
section "inside · collect (${PROFILE})"
# ---------------------------------------------------------------------------
# stage-lfp: low-priv stagepaysan on francpaysan-host
# ---------------------------------------------------------------------------
if [ "$PROFILE" = "stage-lfp" ]; then
SSH_HOST="${INSIDE_SSH:-francpaysan-stage-user}"
BANK_CTR="${INSIDE_BANK_CTR:-stage-lfp-bank}"
EX_CTR="${INSIDE_EXCHANGE_CTR:-stage-lfp-exchange-ansible}"
MER_CTR="${INSIDE_MERCHANT_CTR:-stage-lfp-merchant}"
BANK_PORT="${INSIDE_BANK_PORT:-9032}"
EX_PORT="${INSIDE_EXCHANGE_PORT:-9031}"
MER_PORT="${INSIDE_MERCHANT_PORT:-9030}"
DNS_BANK="${INSIDE_DNS_BANK:-stage.bank.lefrancpaysan.ch}"
DNS_EX="${INSIDE_DNS_EXCHANGE:-stage.exchange.lefrancpaysan.ch}"
DNS_MER="${INSIDE_DNS_MERCHANT:-stage.monnaie.lefrancpaysan.ch}"
# Stage remote has more podman execs than koopa; allow a bit more wall time
STAGE_SSH_T="${INSIDE_SSH_TIMEOUT:-${SSH_CMD_TIMEOUT:-24}}"
if [ "${STAGE_SSH_T}" -lt 24 ] 2>/dev/null; then STAGE_SSH_T=24; fi
if ! mon_ssh_ok "$SSH_HOST"; then
err "ssh" "cannot reach ${SSH_HOST} (stagepaysan low-priv) — set INSIDE_SSH= or SKIP"
summary
exit 1
fi
ok "ssh ${SSH_HOST}" "stagepaysan (podman, no sudo)"
# Inject names/ports into remote (ssh bash -s does not inherit local env).
RAW=$(
{
printf 'BANK_CTR=%q; EX_CTR=%q; MER_CTR=%q\n' "$BANK_CTR" "$EX_CTR" "$MER_CTR"
printf 'BANK_PORT=%q; EX_PORT=%q; MER_PORT=%q\n' "$BANK_PORT" "$EX_PORT" "$MER_PORT"
printf 'DNS_BANK=%q; DNS_EX=%q; DNS_MER=%q\n' "$DNS_BANK" "$DNS_EX" "$DNS_MER"
cat <<'REMOTE'
set +e
emit() { printf 'E|%s|%s|%s|%s\n' "$1" "$2" "$3" "$(printf '%s' "${4:-}" | tr '\n\r' ' ' | head -c 200)"; }
hc() { curl -skS -m 3 -o /tmp/mb -w '%{http_code}' "$1" 2>/dev/null || echo 000; }
hasp() { podman exec "$1" pgrep -f "$2" >/dev/null 2>&1; }
BANK="${BANK_CTR}"
EX="${EX_CTR}"
MER="${MER_CTR}"
podman ps --format '{{.Names}}' 2>/dev/null | grep -qx "$BANK" || BANK=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'stage.*bank|lfp-bank' | head -1)
podman ps --format '{{.Names}}' 2>/dev/null | grep -qx "$EX" || EX=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'stage.*exchange|lfp-exchange' | head -1)
podman ps --format '{{.Names}}' 2>/dev/null | grep -qx "$MER" || MER=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'stage.*merchant|lfp-merchant' | head -1)
check_dns() {
local comp="$1" ctr="$2" host="$3"
local line ip code
[ -n "$host" ] || { emit "$comp" WARN "dns" "empty host"; return 1; }
line=$(podman exec "$ctr" getent ahostsv4 "$host" 2>/dev/null | head -1)
[ -z "$line" ] && line=$(podman exec "$ctr" getent hosts "$host" 2>/dev/null | head -1)
ip=$(echo "$line" | awk '{print $1}')
if [ -z "$ip" ]; then
emit "$comp" WARN "dns $host" "no resolve inside container"
return 1
fi
if [ "$ip" = "127.0.0.1" ] || [ "$ip" = "::1" ]; then
emit "$comp" WARN "dns $host" "$ip (loopback)"
return 1
fi
# 169.254.x = pasta host-gateway pin is OK for stage
code=$(podman exec "$ctr" curl -skS -m 3 -o /dev/null -w '%{http_code}' "https://${host}/config" 2>/dev/null || echo 000)
if [ "$code" = "200" ]; then
emit "$comp" OK "dns $host" "$ip /config=$code"
else
emit "$comp" WARN "dns $host" "$ip /config=$code"
fi
}
if [ -z "$BANK" ]; then emit bank ERROR container "not running"
else
emit bank INFO container "$(podman ps --filter name=^${BANK}$ --format '{{.Names}} {{.Status}}' | head -1)"
emit bank INFO ports "$(podman ps --filter name=^${BANK}$ --format '{{.Ports}}' | head -1)"
hasp "$BANK" 'MainKt serve|libeufin-bank serve' && emit bank OK libeufin "running" || emit bank ERROR libeufin "not running"
podman exec "$BANK" pg_isready -q 2>/dev/null && emit bank OK postgres "ready" || emit bank WARN postgres "pg_isready failed"
c=$(hc "http://127.0.0.1:${BANK_PORT}/config")
[ "$c" = "200" ] && emit bank OK "host :${BANK_PORT}/config" "HTTP $c" || emit bank ERROR "host :${BANK_PORT}/config" "HTTP $c"
c=$(hc "http://127.0.0.1:${BANK_PORT}/taler-integration/config")
[ "$c" = "200" ] && emit bank OK "host integration" "HTTP $c" || emit bank ERROR "host integration" "HTTP $c"
check_dns bank "$BANK" "$DNS_BANK" || true
check_dns bank "$BANK" "$DNS_EX" || true
fi
if [ -z "$EX" ]; then emit exchange ERROR container "not running"
else
emit exchange INFO container "$(podman ps --filter name=^${EX}$ --format '{{.Names}} {{.Status}}' | head -1)"
c=$(hc "http://127.0.0.1:${EX_PORT}/config")
[ "$c" = "200" ] && emit exchange OK "host :${EX_PORT}/config" "HTTP $c" || emit exchange ERROR "host :${EX_PORT}/config" "HTTP $c"
c=$(curl -sS -m 5 -o /dev/null -w '%{http_code}' "http://127.0.0.1:${EX_PORT}/keys" 2>/dev/null || echo 000)
[ "$c" = "200" ] && emit exchange OK "host :${EX_PORT}/keys" "HTTP $c" || emit exchange ERROR "host :${EX_PORT}/keys" "HTTP $c"
hasp "$EX" 'taler-exchange-httpd' && emit exchange OK httpd "running" || emit exchange ERROR httpd "not running"
hasp "$EX" 'taler-exchange-wirewatch' && emit exchange OK wirewatch "running" || emit exchange ERROR wirewatch "not running"
hasp "$EX" 'taler-exchange-aggregator' && emit exchange OK aggregator "running" || emit exchange WARN aggregator "not running"
hasp "$EX" 'taler-exchange-transfer' && emit exchange OK transfer "running" || emit exchange WARN transfer "not running"
check_dns exchange "$EX" "$DNS_BANK" || true
check_dns exchange "$EX" "$DNS_EX" || true
check_dns exchange "$EX" "$DNS_MER" || true
fi
if [ -z "$MER" ]; then emit merchant ERROR container "not running"
else
emit merchant INFO container "$(podman ps --filter name=^${MER}$ --format '{{.Names}} {{.Status}}' | head -1)"
c=$(hc "http://127.0.0.1:${MER_PORT}/config")
[ "$c" = "200" ] && emit merchant OK "host :${MER_PORT}/config" "HTTP $c" || emit merchant ERROR "host :${MER_PORT}/config" "HTTP $c"
hasp "$MER" 'taler-merchant-httpd' && emit merchant OK httpd "running" || emit merchant ERROR httpd "not running"
# Stage merchant often has no wirewatch unit — INFO not WARN
hasp "$MER" 'taler-merchant-wirewatch' && emit merchant OK wirewatch "running" || emit merchant INFO wirewatch "not running (optional on stage)"
hasp "$MER" 'taler-merchant-depositcheck' && emit merchant OK depositcheck "running" || emit merchant INFO depositcheck "not running (optional on stage)"
hasp "$MER" 'taler-merchant-exchangekeyupdate' && emit merchant OK exchangekeyupdate "running" || emit merchant INFO exchangekeyupdate "not running"
check_dns merchant "$MER" "$DNS_BANK" || true
check_dns merchant "$MER" "$DNS_EX" || true
check_dns merchant "$MER" "$DNS_MER" || true
fi
# Caddy is host-wide (often root); stagepaysan can only see the process
if pgrep -x caddy >/dev/null 2>&1; then
emit caddy OK process "running (host)"
else
emit caddy WARN process "not seen as stagepaysan (may still run as root)"
fi
echo DONE
REMOTE
} | mon_ssh_bash "$SSH_HOST" "${STAGE_SSH_T}" || true
)
if [ -z "$RAW" ] || ! echo "$RAW" | grep -q '^E|'; then
err "ssh" "stage remote timed out or empty (cap ${STAGE_SSH_T}s · host=${SSH_HOST})"
summary
exit 1
fi
_last_inside_grp=""
while IFS= read -r line; do
case "$line" in
E\|*)
IFS='|' read -r _ comp level key detail <<<"$line"
case "$comp" in
bank|exchange|merchant|caddy) _g="$comp" ;;
*) _g="ssh" ;;
esac
if [ "$_g" != "$_last_inside_grp" ]; then
set_group "$_g"
_last_inside_grp="$_g"
fi
case "$level" in
OK) ok "[$comp] $key${detail:+ ($detail)}" ;;
ERROR) err "$comp" "$key" "$detail" ;;
WARN) warn "[$comp] $key" "$detail" ;;
INFO) info "[$comp] $key" "$detail" ;;
esac
;;
esac
done <<<"$RAW"
# Stats from outside (laptop) — public HTTPS, no stagepaysan write needed
set_group stats
section "inside · public stats (outside-in)"
_probe_public_stats() {
local comp="$1" url="$2"
local body code age unix now
body=$(mktemp)
code=$(curl -skS -m 8 -o "$body" -w '%{http_code}' "$url" 2>/dev/null || echo 000)
if [ "$code" != "200" ]; then
warn "[$comp] public stats.json" "HTTP $code · $url"
rm -f "$body"
return
fi
unix=$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d.get("generated_at_unix") or 0)' "$body" 2>/dev/null || echo 0)
now=$(date +%s)
if [ "${unix:-0}" -gt 0 ] 2>/dev/null; then
age=$((now - unix))
if [ "$age" -lt 0 ]; then age=0; fi
if [ "$age" -le "${STATS_STALE_SECS:-900}" ]; then
ok "[$comp] public stats.json" "HTTP 200 age=${age}s · $url"
elif [ "$age" -le "${STATS_FAIL_SECS:-3600}" ]; then
warn "[$comp] public stats.json" "stale age=${age}s · $url"
else
err "$comp" "public stats.json" "too old age=${age}s · $url"
fi
else
ok "[$comp] public stats.json" "HTTP 200 · $url"
fi
rm -f "$body"
}
_probe_public_stats bank "https://${DNS_BANK}/intro/stats.json"
_probe_public_stats exchange "https://${DNS_EX}/intro/stats.json"
_probe_public_stats merchant "https://${DNS_MER}/intro/stats.json"
# Host load as stagepaysan (no container RSS from koopa metrics)
set_group load
section "inside · load (stagepaysan host)"
LOAD_LINE=$(mon_ssh_bash "$SSH_HOST" 8 <<'EOF' || true
python3 - <<'PY'
import os
la=os.getloadavg()
print("loadavg=%.2f,%.2f,%.2f" % la)
try:
with open("/proc/meminfo") as f:
d={}
for line in f:
k,v=line.split(":")[0], line.split(":")[1].strip().split()[0]
d[k]=int(v)
total=d.get("MemTotal",0)/1024/1024
avail=d.get("MemAvailable",0)/1024/1024
used=total-avail
print("mem_used=%.2fGiB avail=%.2fGiB total=%.2fGiB" % (used, avail, total))
except Exception:
print("mem=?")
PY
EOF
)
if [ -n "$LOAD_LINE" ]; then
info "stage host" "$(echo "$LOAD_LINE" | tr '\n' ' ')"
else
info "stage host" "load probe empty"
fi
summary
exit 0
fi
# ---------------------------------------------------------------------------
# koopa (default) — existing remote collect
# ---------------------------------------------------------------------------
section "inside · collect from koopa"
if [ "${SKIP_SSH}" = "1" ]; then
warn "ssh" "SKIP_SSH=1 — skipped"
if [ "${SKIP_SSH}" = "1" ] && [ "${LOCAL_STACK:-0}" != "1" ]; then
warn "ssh" "SKIP_SSH=1 and not local — skipped"
summary
exit 0
fi