koopa-admin-log/scripts/taler-monitoring/check_inside.sh
Hernâni Marques 08c55e71a7
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.
2026-07-17 19:17:40 +02:00

418 lines
18 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# 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
source "$ROOT/lib.sh"
# shellcheck source=metrics.sh
source "$ROOT/metrics.sh"
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" ] && [ "${LOCAL_STACK:-0}" != "1" ]; then
warn "ssh" "SKIP_SSH=1 and not local — skipped"
summary
exit 0
fi
if ! koopa_ssh_ok; then
err "ssh" "cannot reach ${KOOPA_SSH} in ${SSH_CONNECT_TIMEOUT}s — set SKIP_SSH=1 to skip inside"
summary
exit 1
fi
ok "ssh ${KOOPA_SSH}"
# One short remote script (≤ SSH_CMD_TIMEOUT). Every slow step is local curl -m 3 or quick pgrep.
RAW=$(
koopa_ssh_bash "${SSH_CMD_TIMEOUT}" <<'REMOTE' || true
set +e
emit() { printf 'E|%s|%s|%s|%s\n' "$1" "$2" "$3" "$(printf '%s' "${4:-}" | tr '\n\r' ' ' | head -c 200)"; }
# quick curl
hc() { curl -skS -m 3 -o /tmp/mb -w '%{http_code}' "$1" 2>/dev/null || echo 000; }
# quick process check inside container (pgrep only)
hasp() { podman exec "$1" pgrep -f "$2" >/dev/null 2>&1; }
BANK=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'hacktivism-bank|taler-bank' | head -1)
[ -z "$BANK" ] && BANK=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -i bank | head -1)
EX=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -i exchange | head -1)
# Exact merchant container name first — never fall through to *-bank
MER=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -E '^taler-hacktivism$' | head -1)
[ -z "$MER" ] && MER=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -i merchant | grep -viE 'bank|exchange' | head -1)
[ -z "$MER" ] && MER=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -E 'hacktivism' | grep -viE 'bank|exchange' | head -1)
# Domain resolve inside container (wirewatch needs bank.hacktivism.ch → real IP)
# emit: comp LEVEL dns "host → ip" or fail
check_dns() {
local comp="$1" ctr="$2" host="$3"
local line ip code
# Prefer IPv4 (wirewatch/libcurl often happier; avoid dead AAAA)
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" ERROR "dns $host" "no resolve — run pin-container-hosts.sh"
return 1
fi
# 127.0.0.1 is almost always wrong for public bank/exchange from inside pasta
if [ "$ip" = "127.0.0.1" ] || [ "$ip" = "::1" ]; then
emit "$comp" ERROR "dns $host" "$ip (loopback — wirewatch will fail)"
return 1
fi
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 — API/withdraw dead"
podman exec "$BANK" pg_isready -q 2>/dev/null && emit bank OK postgres "ready" || emit bank ERROR postgres "not ready"
c=$(hc http://127.0.0.1:9012/config)
[ "$c" = "200" ] && emit bank OK "local /config" "HTTP $c" || emit bank ERROR "local /config" "HTTP $c"
c=$(hc http://127.0.0.1:9012/taler-integration/config)
[ "$c" = "200" ] && emit bank OK "local integration" "HTTP $c" || emit bank ERROR "local integration" "HTTP $c"
hasp "$BANK" 'nginx' && emit bank OK nginx ":9013" || emit bank WARN nginx "not running"
check_dns bank "$BANK" bank.hacktivism.ch || true
check_dns bank "$BANK" exchange.hacktivism.ch || 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:9011/config)
[ "$c" = "200" ] && emit exchange OK "local /config" "HTTP $c" || emit exchange ERROR "local /config" "HTTP $c"
c=$(curl -sS -m 5 -o /dev/null -w '%{http_code}' http://127.0.0.1:9011/keys 2>/dev/null || echo 000)
[ "$c" = "200" ] && emit exchange OK "local /keys" "HTTP $c" || emit exchange ERROR "local /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 — withdraw stuck after bank confirm"
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"
# critical for wire gateway
check_dns exchange "$EX" bank.hacktivism.ch || true
check_dns exchange "$EX" exchange.hacktivism.ch || true
check_dns exchange "$EX" taler.hacktivism.ch || 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 https://127.0.0.1:9010/config)
[ "$c" = "200" ] && emit merchant OK "local /config" "HTTP $c" || emit merchant ERROR "local /config" "HTTP $c"
hasp "$MER" 'taler-merchant-httpd' && emit merchant OK httpd "running" || emit merchant ERROR httpd "not running"
hasp "$MER" 'taler-merchant-wirewatch' && emit merchant OK wirewatch "running" || emit merchant WARN wirewatch "not running"
# depositcheck: may abort on exchange track 404 (merchant assertion) — try ensure once
if hasp "$MER" 'taler-merchant-depositcheck'; then
emit merchant OK depositcheck "running"
else
podman exec "$MER" /usr/local/bin/ensure_merchant_helpers.sh >/dev/null 2>&1 || true
sleep 1
if hasp "$MER" 'taler-merchant-depositcheck'; then
emit merchant OK depositcheck "running (after ensure_merchant_helpers)"
elif hasp "$MER" 'taler-merchant-wirewatch'; then
# Known: depositcheck can SIGABRT on exchange 404 for stale coins — not startable cleanly
emit merchant INFO depositcheck "not running (often aborts on track 404; wirewatch OK · settlement partial)"
else
emit merchant WARN depositcheck "not running"
fi
fi
check_dns merchant "$MER" bank.hacktivism.ch || true
check_dns merchant "$MER" exchange.hacktivism.ch || true
check_dns merchant "$MER" taler.hacktivism.ch || true
fi
if systemctl is-active caddy >/dev/null 2>&1 || pgrep -x caddy >/dev/null 2>&1; then
emit caddy OK process "active"
else
emit caddy ERROR process "not active"
fi
echo DONE
REMOTE
)
if [ -z "$RAW" ] || ! echo "$RAW" | grep -q '^E|'; then
err "ssh" "remote timed out or empty (cap ${SSH_CMD_TIMEOUT}s)"
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"
# Group IDs by component so issues map cleanly: inside.bank-02, inside.exchange-04
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"
# Host loadavg + RAM + per-container RSS/CPU (same probe as e2e/ladder)
set_group load
section "inside · load / memory"
METRICS_DIR="${METRICS_DIR:-$(mktemp -d)}"
export METRICS_DIR
metrics_report_load "${METRICS_DIR}/load-inside.json" "inside" || true
summary