scripts: sanity/dns/wallet-cli helpers; ops cleanup

This commit is contained in:
Hernâni Marques 2026-07-09 19:57:14 +02:00
parent 1181680a4b
commit be05666737
No known key found for this signature in database
18 changed files with 1440 additions and 0 deletions

View file

@ -0,0 +1,31 @@
# Taler sanity checks (koopa)
Run **on koopa as root** (reads `/root/*-password.txt`, may use `podman` as `hernani`).
| Script | What it checks |
|--------|----------------|
| `check_helpers-running.sh` | **No systemd:** starts missing exchange/merchant helpers (`ensure_*`), then requires transfer/aggregator/wirewatch/… |
| `check_stack-health.sh` | Public + local `/config` and exchange `/keys` HTTP 200; merchant has CHF+GOA |
| `check_merchant-delays.sh` | Global + instance delays are short (pay/refund/wire ≤ 120s) |
| `check_exchange-wirewatch.sh` | Wire gateway reachable with **bearer**; auth method; hosts pin |
| `check_settlement.sh` | Paid orders: `wired`, deposit_total, bank balances/transfers |
| `run_all.sh` | Runs all of the above |
Helper ensure scripts (in containers, no systemd):
| Script | Role |
|--------|------|
| `../taler-exchange/ensure_exchange_helpers.sh` | `nohup` aggregator, closer, wirewatch, **transfer** |
| `../taler-merchant/ensure_merchant_helpers.sh` | `nohup` wirewatch, depositcheck, kyccheck, webhook, … |
| Health: `check_*-health.sh` | Calls ensure (unless `SKIP_ENSURE=1`), then **FAIL** if still missing |
```bash
# on koopa
cd /path/to/koopa-admin-log/scripts/taler-sanity
./run_all.sh
./check_settlement.sh 2026.190-03V36SPAZ8CK6
```
Env overrides: `BANK_URL`, `MERCHANT_URL`, `EXCHANGE_PUBLIC`, `BANK_PUBLIC`, `MERCHANT_PUBLIC`, `MERCHANT_INSTANCE`.
See also: `../taler-wallet-cli/` for end-to-end withdraw → pay → settlement.

View file

@ -0,0 +1,92 @@
#!/bin/bash
# Sanity: exchange can read bank wire gateway (bearer + public URL).
# Run on koopa as root (needs /root/bank-exchange-password.txt + podman).
set -euo pipefail
ROOT=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=lib.sh
source "$ROOT/lib.sh"
need_root
echo "=== Exchange wirewatch / wire gateway ==="
if ! read_pw EPW /root/bank-exchange-password.txt; then
fail "missing /root/bank-exchange-password.txt"
exit 1
fi
ET=$(bank_token exchange "$EPW")
if [ -z "$ET" ]; then
fail "could not get bank token for exchange user"
exit 1
fi
pass "bank token for exchange user"
# History must work with Bearer (Basic is rejected by libeufin on history)
tmp=$(mktemp)
code=$(curl -sk -m 15 -o "$tmp" -w '%{http_code}' \
-H "Authorization: Bearer ${ET}" \
"${BANK_PUBLIC}/accounts/exchange/taler-wire-gateway/history/incoming?delta=-10")
if [ "$code" = "200" ]; then
pass "wire gateway history via ${BANK_PUBLIC} (HTTP $code)"
python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(" incoming_count", len(d.get("incoming_transactions") or []))' "$tmp"
else
# fallback local
code2=$(curl -sS -m 15 -o "$tmp" -w '%{http_code}' \
-H "Authorization: Bearer ${ET}" \
"${BANK_URL}/accounts/exchange/taler-wire-gateway/history/incoming?delta=-10")
if [ "$code2" = "200" ]; then
warn "public wire gateway HTTP $code; local BANK_URL OK ($code2) — check DNS/hosts in exchange container"
else
fail "wire gateway history public=$code local=$code2"
fi
fi
rm -f "$tmp"
# Basic must fail on history (documents expected auth mode)
code_b=$(curl -sk -m 10 -o /dev/null -w '%{http_code}' \
-u "exchange:${EPW}" \
"${BANK_URL}/accounts/exchange/taler-wire-gateway/history/incoming?delta=-5")
if [ "$code_b" = "401" ]; then
pass "Basic auth correctly rejected on history ($code_b) — use bearer TOKEN"
else
warn "Basic on history returned $code_b (expected 401)"
fi
# Container credential + wirewatch process
if su - hernani -c 'podman exec taler-exchange-hacktivism true' 2>/dev/null; then
su - hernani -c 'podman exec taler-exchange-hacktivism bash -c "
set +e
echo \"--- secret (redacted) ---\"
if [ -r /etc/taler-exchange/secrets/exchange-accountcredentials-1.secret.conf ]; then
sed \"s/secret-token:.*/secret-token:***/; s/^TOKEN = .*/TOKEN = ***/; s/^PASSWORD = .*/PASSWORD = ***/\" \
/etc/taler-exchange/secrets/exchange-accountcredentials-1.secret.conf
else
# root-only file: cat as root via outer exec
true
fi
echo \"--- hosts bank ---\"
grep bank.hacktivism.ch /etc/hosts || echo \"(no hosts pin)\"
echo \"--- wirewatch ---\"
ps -eo pid,stat,args | grep \"[w]irewatch\" || echo \"(no wirewatch process)\"
"' 2>&1 || warn "podman exec exchange failed"
AUTH_METHOD=$(su - hernani -c 'podman exec -u root taler-exchange-hacktivism \
grep -E "^WIRE_GATEWAY_AUTH_METHOD" /etc/taler-exchange/secrets/exchange-accountcredentials-1.secret.conf' 2>/dev/null | awk '{print $3}')
GW_URL=$(su - hernani -c 'podman exec -u root taler-exchange-hacktivism \
grep -E "^WIRE_GATEWAY_URL" /etc/taler-exchange/secrets/exchange-accountcredentials-1.secret.conf' 2>/dev/null | awk '{print $3}')
info "WIRE_GATEWAY_URL=$GW_URL"
info "WIRE_GATEWAY_AUTH_METHOD=$AUTH_METHOD"
if [ "$AUTH_METHOD" = "bearer" ]; then pass "auth method bearer"
else fail "auth method is '$AUTH_METHOD' (want bearer + TOKEN=)"
fi
case "$GW_URL" in
https://bank.hacktivism.ch/*) pass "gateway uses public bank URL" ;;
http://127.*|http://host.containers*) warn "gateway uses local URL: $GW_URL" ;;
*) warn "unexpected gateway URL: $GW_URL" ;;
esac
else
warn "exchange container not reachable via podman"
fi
echo "=== summary fails=$FAILS ==="
exit "$FAILS"

View file

@ -0,0 +1,81 @@
#!/bin/bash
# Sanity: ensure exchange + merchant helpers are up (no systemd).
# Runs on koopa as root; uses podman into both containers.
set -euo pipefail
ROOT=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=lib.sh
source "$ROOT/lib.sh"
need_root
fail=0
pass() { echo "OK $1"; }
warn() { echo "WARN $1"; }
bad() { echo "FAIL $1"; fail=$((fail + 1)); }
EXC_CT="${EXCHANGE_CONTAINER:-taler-exchange-hacktivism}"
MER_CT="${MERCHANT_CONTAINER:-taler-hacktivism}"
echo "=== ensure + check helpers (no systemd) ==="
# Deploy/ensure scripts if present on host under admin-log mount or /tmp
deploy_into() {
local ct="$1"
local src="$2"
local dst="$3"
if [ -f "$src" ]; then
su - hernani -c "podman cp $(printf %q "$src") ${ct}:${dst}" 2>/dev/null \
|| podman cp "$src" "${ct}:${dst}" 2>/dev/null || true
su - hernani -c "podman exec ${ct} chmod 755 ${dst}" 2>/dev/null || true
fi
}
ADMIN="${KOOPA_ADMIN_LOG:-/home/hernani/koopa-admin-log}"
# Prefer workspace copy if synced; else scripts next to this file
EX_SCRIPTS="$ROOT/../taler-exchange"
MER_SCRIPTS="$ROOT/../taler-merchant"
[ -d "$ADMIN/scripts/taler-exchange" ] && EX_SCRIPTS="$ADMIN/scripts/taler-exchange"
[ -d "$ADMIN/scripts/taler-merchant" ] && MER_SCRIPTS="$ADMIN/scripts/taler-merchant"
deploy_into "$EXC_CT" "$EX_SCRIPTS/ensure_exchange_helpers.sh" /usr/local/bin/ensure_exchange_helpers.sh
deploy_into "$EXC_CT" "$EX_SCRIPTS/check_exchange-health.sh" /usr/local/bin/check_exchange-health.sh
deploy_into "$MER_CT" "$MER_SCRIPTS/ensure_merchant_helpers.sh" /usr/local/bin/ensure_merchant_helpers.sh
deploy_into "$MER_CT" "$MER_SCRIPTS/check_merchant-health.sh" /usr/local/bin/check_merchant-health.sh
echo "--- exchange container: ensure ---"
if su - hernani -c "podman exec $EXC_CT /usr/local/bin/ensure_exchange_helpers.sh"; then
pass "exchange helpers ensure"
else
bad "exchange helpers ensure failed"
fi
echo "--- merchant container: ensure ---"
if su - hernani -c "podman exec $MER_CT /usr/local/bin/ensure_merchant_helpers.sh"; then
pass "merchant helpers ensure"
else
bad "merchant helpers ensure failed"
fi
echo "--- process presence ---"
check_ct_proc() {
local ct="$1"
local p="$2"
# COMM is 15 chars — match full cmdline
if su - hernani -c "podman exec $ct pgrep -f '(^|/)${p}( |$)'" >/dev/null 2>&1; then
pass "$ct: $p"
else
bad "$ct: $p missing"
fi
}
for p in taler-exchange-httpd taler-exchange-aggregator taler-exchange-transfer \
taler-exchange-wirewatch taler-exchange-closer; do
check_ct_proc "$EXC_CT" "$p"
done
for p in taler-merchant-httpd taler-merchant-wirewatch taler-merchant-depositcheck \
taler-merchant-webhook taler-merchant-kyccheck \
taler-merchant-exchangekeyupdate taler-merchant-reconciliation; do
check_ct_proc "$MER_CT" "$p"
done
echo "=== summary fails=$fail ==="
exit "$fail"

View file

@ -0,0 +1,72 @@
#!/bin/bash
# Sanity: merchant global + demo instance delays are demo-short (pay/refund/wire).
# Run on koopa (network to :9010 / public merchant).
set -euo pipefail
ROOT=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=lib.sh
source "$ROOT/lib.sh"
# Expected upper bounds (seconds) — fail if longer (package defaults 1d/15d/1w)
MAX_PAY_S=${MAX_PAY_S:-120}
MAX_REFUND_S=${MAX_REFUND_S:-120}
MAX_WIRE_S=${MAX_WIRE_S:-120}
echo "=== Merchant delay defaults (expect short demo values) ==="
tmp=$(mktemp)
curl -sk -m 12 -o "$tmp" "${MERCHANT_PUBLIC}/config" || curl -sk -m 12 -o "$tmp" "${MERCHANT_URL}/config"
python3 - "$tmp" "$MAX_PAY_S" "$MAX_REFUND_S" "$MAX_WIRE_S" <<'PY'
import json,sys
d=json.load(open(sys.argv[1]))
max_pay, max_ref, max_wire = map(float, sys.argv[2:5])
fails=0
for key, mx in (
("default_pay_delay", max_pay),
("default_refund_delay", max_ref),
("default_wire_transfer_delay", max_wire),
):
us=d.get(key,{}).get("d_us")
s=(us or 0)/1e6
ok = us is not None and s <= mx
print(f"{'OK' if ok else 'FAIL'} {key}: {s:.0f}s (max {mx:.0f}s)")
if not ok: fails+=1
print("currency", d.get("currency"))
sys.exit(fails)
PY
ec=$?
rm -f "$tmp"
FAILS=$((FAILS + ec))
# Instance goa-demo if password present
if [ -f /root/merchant-goa-demo-cp4zqk-password.txt ]; then
MPW=$(tr -d '\n' </root/merchant-goa-demo-cp4zqk-password.txt)
tmp=$(mktemp)
curl -sk -m 12 -H "Authorization: Bearer secret-token:${MPW}" \
-o "$tmp" "${MERCHANT_URL}/instances/goa-demo-cp4zqk/private/" || true
if [ -s "$tmp" ]; then
python3 - "$tmp" "$MAX_PAY_S" "$MAX_REFUND_S" "$MAX_WIRE_S" <<'PY'
import json,sys
d=json.load(open(sys.argv[1]))
max_pay, max_ref, max_wire = map(float, sys.argv[2:5])
fails=0
for key, mx in (
("default_pay_delay", max_pay),
("default_refund_delay", max_ref),
("default_wire_transfer_delay", max_wire),
):
us=d.get(key,{}).get("d_us")
s=(us or 0)/1e6
ok = us is not None and s <= mx
print(f"{'OK' if ok else 'FAIL'} instance {key}: {s:.0f}s")
if not ok: fails+=1
sys.exit(fails)
PY
FAILS=$((FAILS + $?))
else
warn "could not load instance goa-demo-cp4zqk"
fi
rm -f "$tmp"
fi
echo "=== summary fails=$FAILS ==="
exit "$FAILS"

View file

@ -0,0 +1,119 @@
#!/bin/bash
# Sanity / ops: check paid orders wired? bank transfers? balances?
# Run on koopa as root.
#
# Usage:
# check_settlement.sh # all paid orders for goa-demo
# check_settlement.sh ORDER_ID # one order
# MERCHANT_INSTANCE=foo check_settlement.sh
set -euo pipefail
ROOT=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=lib.sh
source "$ROOT/lib.sh"
need_root
INST="${MERCHANT_INSTANCE:-goa-demo-cp4zqk}"
ORDER_ID="${1:-}"
if ! read_pw MPW "/root/merchant-${INST}-password.txt"; then
read_pw MPW /root/merchant-goa-demo-cp4zqk-password.txt || true
fi
if [ -z "${MPW:-}" ]; then
fail "no merchant password for $INST"
exit 1
fi
AUTH=$(merchant_auth_header "$MPW")
echo "=== Settlement check instance=$INST ==="
tmp=$(mktemp)
curl -sk -m 15 -H "$AUTH" -o "$tmp" \
"${MERCHANT_URL}/instances/${INST}/private/orders?paid=YES"
python3 - "$tmp" "$ORDER_ID" <<'PY'
import json,sys,time
d=json.load(open(sys.argv[1]))
want=sys.argv[2] or None
orders=d.get("orders") or []
if want:
orders=[o for o in orders if o.get("order_id")==want]
if not orders:
print("FAIL no paid orders" + (f" matching {want}" if want else ""))
sys.exit(1)
print(f"INFO paid_orders={len(orders)}")
for o in orders:
print(f" - {o.get('order_id')} amount={o.get('amount')} summary={o.get('summary')!r} paid={o.get('paid')}")
sys.exit(0)
PY
ec=$?
[ "$ec" -eq 0 ] || FAILS=$((FAILS + 1))
# Detail each order
mapfile -t OIDS < <(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); want=sys.argv[2] or None
oids=[o["order_id"] for o in d.get("orders",[])]
print("\n".join([x for x in oids if not want or x==want]))' "$tmp" "$ORDER_ID")
for oid in "${OIDS[@]}"; do
echo "--- order $oid ---"
ot=$(mktemp)
curl -sk -m 15 -H "$AUTH" -o "$ot" \
"${MERCHANT_URL}/instances/${INST}/private/orders/${oid}"
python3 - "$ot" <<'PY'
import json,sys,time
d=json.load(open(sys.argv[1]))
ct=d.get("contract_terms") or {}
ts=(ct.get("timestamp") or {}).get("t_s")
rd=(ct.get("refund_deadline") or {}).get("t_s")
wd=(ct.get("wire_transfer_deadline") or {}).get("t_s")
now=int(time.time())
print(" status", d.get("order_status"), "wired", d.get("wired"), "deposit_total", d.get("deposit_total"))
print(" amount", ct.get("amount"), "summary", ct.get("summary"))
if ts and rd and wd:
print(f" age_s={now-ts} refund_in_s={rd-now} wire_in_s={wd-now}")
print(f" windows: refund={rd-ts}s wire_from_pay={wd-ts}s (wire_after_refund={wd-rd}s)")
if now >= wd and not d.get("wired"):
print(" FAIL wire deadline passed but wired=false")
sys.exit(2)
if now < wd:
print(" INFO still before wire deadline — settlement not due yet")
if d.get("wired"):
print(" OK wired=true")
sys.exit(0)
PY
ec=$?
[ "$ec" -eq 2 ] && FAILS=$((FAILS + 1))
rm -f "$ot"
done
echo "=== private/transfers ==="
tr=$(mktemp)
curl -sk -m 15 -H "$AUTH" -o "$tr" \
"${MERCHANT_URL}/instances/${INST}/private/transfers"
python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); t=d.get("transfers") or []; print(f" transfers={len(t)}");
[print(" ", x) for x in t[:10]]' "$tr"
rm -f "$tr" "$tmp"
# Bank balances
BANK_USER="${BANK_USER:-$INST}"
if read_pw BPW "/root/bank-${BANK_USER}-password.txt" || read_pw BPW /root/bank-goa-demo-cp4zqk-password.txt; then
BT=$(bank_token "$BANK_USER" "$BPW")
if [ -n "$BT" ]; then
curl -sS -m 12 -H "Authorization: Bearer ${BT}" \
"${BANK_URL}/accounts/${BANK_USER}" | python3 -c 'import sys,json; d=json.load(sys.stdin); print("merchant_bank_balance", d.get("balance"))'
curl -sS -m 12 -H "Authorization: Bearer ${BT}" \
"${BANK_URL}/accounts/${BANK_USER}/transactions?delta=-10" \
| python3 -c 'import sys,json; d=json.load(sys.stdin); txs=d.get("transactions") or []; print(f"merchant_bank_tx={len(txs)}");
[print(f" {t.get(\"direction\")} {t.get(\"amount\")} {t.get(\"subject\",\"\")[:60]}") for t in txs[:8]]'
fi
fi
if read_pw EPW /root/bank-exchange-password.txt; then
ET=$(bank_token exchange "$EPW")
if [ -n "$ET" ]; then
curl -sS -m 12 -H "Authorization: Bearer ${ET}" \
"${BANK_URL}/accounts/exchange" | python3 -c 'import sys,json; d=json.load(sys.stdin); print("exchange_bank_balance", d.get("balance"))'
fi
fi
echo "=== summary fails=$FAILS ==="
exit "$FAILS"

View file

@ -0,0 +1,73 @@
#!/bin/bash
# Sanity: public + local Taler endpoints respond (GOA stack).
# Run on koopa as root (or any user with network to services).
set -euo pipefail
ROOT=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=lib.sh
source "$ROOT/lib.sh"
echo "=== Taler stack health ==="
# Public HTTPS (via Caddy / VeciGate)
for url in \
"${EXCHANGE_PUBLIC}/config" \
"${EXCHANGE_PUBLIC}/keys" \
"${BANK_PUBLIC}/config" \
"${MERCHANT_PUBLIC}/config"
do
code=$(http_code "$url")
if [ "$code" = "200" ]; then pass "$url -> $code"
else fail "$url -> $code"
fi
done
# Local loopback ports (containers via pasta)
for spec in \
"bank ${BANK_URL}/config" \
"merchant ${MERCHANT_URL}/config" \
"exchange-via-public ${EXCHANGE_PUBLIC}/config"
do
name=${spec%% *}
url=${spec#* }
code=$(http_code "$url")
if [ "$code" = "200" ]; then pass "local $name -> $code"
else fail "local $name -> $code"
fi
done
# Merchant multi-currency
tmp=$(mktemp)
curl -sk -m 12 -o "$tmp" "${MERCHANT_PUBLIC}/config" || true
if python3 - "$tmp" <<'PY'
import json,sys
d=json.load(open(sys.argv[1]))
ex=[e.get("currency") for e in d.get("exchanges") or []]
cur=list((d.get("currencies") or {}).keys())
ok = "GOA" in ex and "CHF" in ex and "GOA" in cur
print("currency_default", d.get("currency"))
print("exchanges", ex)
print("currencies", cur)
sys.exit(0 if ok else 1)
PY
then pass "merchant has CHF + GOA exchanges"
else fail "merchant missing CHF/GOA in /config"
fi
rm -f "$tmp"
# Exchange currency GOA
tmp=$(mktemp)
curl -sk -m 12 -o "$tmp" "${EXCHANGE_PUBLIC}/config" || true
if python3 - "$tmp" <<'PY'
import json,sys
d=json.load(open(sys.argv[1]))
c=(d.get("currency") or d.get("currency_specification",{}).get("currency"))
print("exchange_currency", c)
sys.exit(0 if c=="GOA" else 1)
PY
then pass "exchange currency GOA"
else fail "exchange currency not GOA"
fi
rm -f "$tmp"
echo "=== summary fails=$FAILS ==="
exit "$FAILS"

View file

@ -0,0 +1,60 @@
# shellcheck shell=bash
# Common helpers for Taler sanity checks (run on koopa host as root).
# shellcheck disable=SC2034
: "${BANK_URL:=http://127.0.0.1:9012}"
: "${MERCHANT_URL:=https://127.0.0.1:9010}"
: "${EXCHANGE_PUBLIC:=https://exchange.hacktivism.ch}"
: "${BANK_PUBLIC:=https://bank.hacktivism.ch}"
: "${MERCHANT_PUBLIC:=https://taler.hacktivism.ch}"
pass() { echo "OK $*"; }
fail() { echo "FAIL $*"; FAILS=$((FAILS + 1)); }
warn() { echo "WARN $*"; }
info() { echo "INFO $*"; }
FAILS=0
need_root() {
if [ "$(id -u)" -ne 0 ]; then
echo "This check expects root on koopa (password files under /root)." >&2
exit 2
fi
}
read_pw() {
# read_pw VAR path
local _v="$1" _p="$2"
if [ ! -f "$_p" ]; then
eval "$_v="
return 1
fi
eval "$_v=\$(tr -d '\\n' <\"$_p\")"
}
bank_token() {
# bank_token USER PASS -> prints access_token
local user="$1" pass="$2"
curl -sS -m 15 -u "${user}:${pass}" \
-H 'Content-Type: application/json' \
-d '{"scope":"readwrite","refreshable":true}' \
"${BANK_URL}/accounts/${user}/token" \
| python3 -c 'import sys,json; print(json.load(sys.stdin).get("access_token",""))'
}
merchant_auth_header() {
# merchant_auth_header PASSWORD
printf 'Authorization: Bearer secret-token:%s' "$1"
}
http_code() {
# http_code URL [curl args...]
local url="$1"
shift
curl -sk -m 12 -o /dev/null -w '%{http_code}' "$@" "$url" 2>/dev/null || echo "000"
}
json_get() {
# json_get FILE python-expr-on-d
python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print($2)" "$1"
}

View file

@ -0,0 +1,22 @@
#!/bin/bash
# Run all sanity checks; exit non-zero if any failed.
set -euo pipefail
ROOT=$(cd "$(dirname "$0")" && pwd)
ec=0
for s in \
check_helpers-running.sh \
check_stack-health.sh \
check_merchant-delays.sh \
check_exchange-wirewatch.sh \
check_settlement.sh
do
echo ""
echo "########## $s ##########"
if bash "$ROOT/$s"; then
echo "PASS $s"
else
echo "FAIL $s (exit $?)"
ec=1
fi
done
exit $ec