Compare commits
8 commits
d6f4f649c1
...
e46aeb408a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e46aeb408a | ||
|
|
3d5f412af3 | ||
|
|
62d0e426f4 | ||
|
|
d2113e7362 | ||
|
|
b6c3225e97 | ||
|
|
e1cc6d7141 | ||
|
|
f8affeb651 | ||
|
|
3d4e883cdb |
19 changed files with 1204 additions and 465 deletions
|
|
@ -14,12 +14,14 @@ TimeoutStartSec=480
|
|||
# Do not keep "active" after run — timer may fire again cleanly
|
||||
RemainAfterExit=no
|
||||
Environment=TZ=Europe/Zurich
|
||||
# Stack overrides (goa / stage-testpaysan / custom) — optional
|
||||
EnvironmentFile=-%h/.config/taler-landing/stack.env
|
||||
# GOA defaults if stack.env absent
|
||||
Environment=ADMIN_LOG=%h/src/koopa/koopa-admin-log
|
||||
Environment=SECRETS_BANK=%h/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank
|
||||
Environment=BANK_URL=http://127.0.0.1:9012
|
||||
Environment=BANK_PUBLIC_URL=https://bank.hacktivism.ch
|
||||
Environment=EXCHANGE_CONFIG_URL=https://exchange.hacktivism.ch/config
|
||||
# All accounts except exchange (double-count of withdraw credits on exchange ledger)
|
||||
Environment=SCAN_SKIP=exchange
|
||||
Environment=TX_PAGE=500
|
||||
Environment=MAX_TX_PAGES=0
|
||||
|
|
|
|||
|
|
@ -238,6 +238,7 @@ fi
|
|||
|
||||
GEN=$(now_iso)
|
||||
HUMAN=$(now_human)
|
||||
GEN_UNIX=$(date +%s)
|
||||
num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; }
|
||||
|
||||
MEM_JSON='"container_rss_human": "—"'
|
||||
|
|
@ -257,6 +258,7 @@ cat >"$TMP" <<EOF
|
|||
"timezone": $(json_str "$TZ"),
|
||||
"generated_at": $(json_str "$GEN"),
|
||||
"generated_at_human": $(json_str "$HUMAN"),
|
||||
"generated_at_unix": ${GEN_UNIX},
|
||||
"reserves": ${RESERVES:-0},
|
||||
"wire_in_count": ${RESERVES_IN:-0},
|
||||
"wire_in_amount": $(json_str "$WIRE_IN_AMT"),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,21 @@
|
|||
# taler-landing — central stats (hernani user systemd)
|
||||
# taler-landing — generic multi-stack landing stats
|
||||
|
||||
Public landing numbers on
|
||||
One collector (`collect_bank_stats.py` + `collect-landing-stats.sh`) for any
|
||||
currency / stack. **Defaults = GOA / hacktivism**; stage TESTPAYSAN uses a
|
||||
profile file.
|
||||
|
||||
Public landing numbers (example GOA):
|
||||
|
||||
- https://bank.hacktivism.ch/intro/ → `stats.json`
|
||||
- https://exchange.hacktivism.ch/intro/ → `stats.json`
|
||||
- https://taler.hacktivism.ch/intro/ → `stats.json`
|
||||
|
||||
are produced by **one host process** as user **`hernani`**, not by three unrelated in-container crons.
|
||||
Stage TESTPAYSAN (shared bank feed on all three hosts):
|
||||
|
||||
- https://stage.bank.lefrancpaysan.ch/intro/stats.json
|
||||
(same file also under stage.exchange / stage.monnaie landings)
|
||||
|
||||
Produced by **one host process** (user systemd timer), not three crons.
|
||||
|
||||
## Why host / user systemd
|
||||
|
||||
|
|
@ -77,16 +86,37 @@ Preferred order:
|
|||
|
||||
Explorer password optional (shared-pool balance).
|
||||
|
||||
## Profiles
|
||||
|
||||
| File | Stack |
|
||||
|------|--------|
|
||||
| `profiles/goa.env` | hacktivism GOA (container publish) |
|
||||
| `profiles/stage-testpaysan.env` | FrancPaysan stage; **shared** host `stats.json` |
|
||||
|
||||
```bash
|
||||
# Stage (on stagepaysan):
|
||||
STACK_PROFILE=stage-testpaysan ./scripts/taler-landing/install-landing-stats-host.sh
|
||||
# or: francpaysan-admin-log/scripts/stagepaysan/install-landing-stats.sh
|
||||
```
|
||||
|
||||
`~/.config/taler-landing/stack.env` is sourced on each run.
|
||||
|
||||
## Env overrides
|
||||
|
||||
| Env | Default | Meaning |
|
||||
|-----|---------|---------|
|
||||
| `BANK_URL` | `http://127.0.0.1:9012` | libeufin loopback |
|
||||
| `BANK_CURRENCY` | *(auto)* | `GOA` / `TESTPAYSAN` / … |
|
||||
| `SCAN_SKIP` | `exchange` | usernames excluded from flow |
|
||||
| `COLLECT_BANK` / `_EXCHANGE` / `_MERCHANT` | `1` | phase switches |
|
||||
| `COLLECT_RESOURCES` | `1` | container RSS/loadavg merge |
|
||||
| `HOST_STATS_DIRS` | empty | space-separated host dirs for **shared** bank `stats.json` |
|
||||
| `PUBLISH_PODMAN` | `1` | also `podman cp` into container landings |
|
||||
| `TX_PAGE` | `500` | transactions page size |
|
||||
| `MAX_TX_PAGES` | `0` | `0` = unlimited pages / account |
|
||||
| `ACCOUNTS_DELTA` | `-10000` | account list window |
|
||||
| `ADMIN_LOG` | `%h/src/koopa/koopa-admin-log` | refresh in-container scripts |
|
||||
| `STATS_SOURCE_LABEL` | `host collect_bank_stats.py` | `stats.source` field |
|
||||
|
||||
## UI alt names
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
#!/usr/bin/env bash
|
||||
# Central landing-stats collector for hernani@koopa (user systemd timer).
|
||||
# Generic landing-stats orchestrator (any Taler stack).
|
||||
#
|
||||
# - Bank: full account+ledger scan via collect_bank_stats.py → bank container
|
||||
# - Exchange / merchant: existing in-container scripts, then amount_alt enrich
|
||||
# - All three: container RSS / loadavg / top procs via mem-snapshot (podman exec)
|
||||
# - Never wipes a good stats.json on failure (writes stats-run.json only)
|
||||
# Defaults = GOA / hacktivism on koopa. Override via env or EnvironmentFile
|
||||
# (see profiles: goa | stage-testpaysan).
|
||||
#
|
||||
# - Bank: collect_bank_stats.py (currency-agnostic) → stats.json
|
||||
# - Optional: exchange / merchant in-container generators
|
||||
# - Optional: container RSS/loadavg merge
|
||||
# - Publish: podman into container landings and/or host dirs (shared bank stats)
|
||||
# - Never wipes a good stats.json on failure (stats-run.json only)
|
||||
#
|
||||
# Install: scripts/taler-landing/install-landing-stats-host.sh
|
||||
set -euo pipefail
|
||||
|
|
@ -12,6 +16,20 @@ set -euo pipefail
|
|||
export TZ="${TZ:-Europe/Zurich}"
|
||||
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin${PATH:+:$PATH}"
|
||||
|
||||
# Optional stack profile: ~/.config/taler-landing/stack.env or STACK_ENV_FILE
|
||||
if [ -n "${STACK_ENV_FILE:-}" ] && [ -f "${STACK_ENV_FILE}" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "${STACK_ENV_FILE}"
|
||||
set +a
|
||||
elif [ -f "${HOME}/.config/taler-landing/stack.env" ]; then
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "${HOME}/.config/taler-landing/stack.env"
|
||||
set +a
|
||||
fi
|
||||
|
||||
log() { printf '%s %s\n' "$(date -Iseconds)" "$*"; }
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
|
@ -22,6 +40,8 @@ elif [ -f "${HOME}/.local/lib/taler-landing/collect_bank_stats.py" ]; then
|
|||
LIB="${HOME}/.local/lib/taler-landing"
|
||||
elif [ -f "${HOME}/src/koopa/koopa-admin-log/scripts/taler-landing/collect_bank_stats.py" ]; then
|
||||
LIB="${HOME}/src/koopa/koopa-admin-log/scripts/taler-landing"
|
||||
elif [ -f "${HOME}/taler/src/koopa-admin-log/scripts/taler-landing/collect_bank_stats.py" ]; then
|
||||
LIB="${HOME}/taler/src/koopa-admin-log/scripts/taler-landing"
|
||||
else
|
||||
LIB="$ROOT"
|
||||
fi
|
||||
|
|
@ -29,6 +49,13 @@ fi
|
|||
ADMIN_LOG="${ADMIN_LOG:-${HOME}/src/koopa/koopa-admin-log}"
|
||||
SECRETS_BANK="${SECRETS_BANK:-${HOME}/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank}"
|
||||
|
||||
# Which phases to run (1/0)
|
||||
COLLECT_BANK="${COLLECT_BANK:-1}"
|
||||
COLLECT_EXCHANGE="${COLLECT_EXCHANGE:-1}"
|
||||
COLLECT_MERCHANT="${COLLECT_MERCHANT:-1}"
|
||||
COLLECT_RESOURCES="${COLLECT_RESOURCES:-1}"
|
||||
|
||||
# Containers (empty = skip podman publish for that role)
|
||||
BANK_CTR="${BANK_CTR:-taler-hacktivism-bank}"
|
||||
EX_CTR="${EX_CTR:-taler-hacktivism-exchange-ansible}"
|
||||
MER_CTR="${MER_CTR:-taler-hacktivism}"
|
||||
|
|
@ -36,10 +63,19 @@ MER_CTR="${MER_CTR:-taler-hacktivism}"
|
|||
BANK_URL="${BANK_URL:-http://127.0.0.1:9012}"
|
||||
BANK_PUBLIC_URL="${BANK_PUBLIC_URL:-https://bank.hacktivism.ch}"
|
||||
EXCHANGE_CONFIG_URL="${EXCHANGE_CONFIG_URL:-https://exchange.hacktivism.ch/config}"
|
||||
BANK_CURRENCY="${BANK_CURRENCY:-}" # empty → auto from bank/exchange /config
|
||||
STATS_SOURCE_LABEL="${STATS_SOURCE_LABEL:-host collect_bank_stats.py}"
|
||||
|
||||
# In-container landing roots (podman publish)
|
||||
BANK_LANDING_IN="${BANK_LANDING_IN:-/var/www/bank-landing}"
|
||||
EX_LANDING_IN="${EX_LANDING_IN:-/var/www/exchange-landing}"
|
||||
MER_LANDING_IN="${MER_LANDING_IN:-/var/www/merchant-landing}"
|
||||
PUBLISH_PODMAN="${PUBLISH_PODMAN:-1}"
|
||||
|
||||
# Host dirs that should get the *same* bank stats.json (shared public feed).
|
||||
# Colon-separated (systemd EnvironmentFile-safe). Example stage:
|
||||
# HOST_STATS_DIRS=/var/www/lfp-stage/bank:/var/www/lfp-stage/exchange:/var/www/lfp-stage/merchant
|
||||
HOST_STATS_DIRS="${HOST_STATS_DIRS:-}"
|
||||
|
||||
WORKDIR="${LANDING_STATS_WORKDIR:-${XDG_RUNTIME_DIR:-/tmp}/taler-landing-stats}"
|
||||
mkdir -p "$WORKDIR"
|
||||
|
|
@ -64,7 +100,9 @@ MEM_SRC="${MEM_SNAPSHOT_SRC:-$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh}"
|
|||
|
||||
merge_container_resources() {
|
||||
local label="$1" ctr="$2" stats_file="$3"
|
||||
[ "${COLLECT_RESOURCES}" = "1" ] || return 0
|
||||
[ -f "$stats_file" ] || return 0
|
||||
[ -n "$ctr" ] || return 0
|
||||
if ! ctr_running "$ctr"; then
|
||||
log "WARN: $label resources: $ctr not running"
|
||||
return 1
|
||||
|
|
@ -95,6 +133,36 @@ merge_container_resources() {
|
|||
return 0
|
||||
}
|
||||
|
||||
# Write stats.json into host directories (shared feed: same file everywhere).
|
||||
publish_host_stats() {
|
||||
local src_stats="$1" src_run="${2:-}"
|
||||
[ -n "${HOST_STATS_DIRS:-}" ] || return 0
|
||||
[ -f "$src_stats" ] || return 0
|
||||
local d paths
|
||||
# Accept colon or space separators
|
||||
paths=$(printf '%s' "$HOST_STATS_DIRS" | tr ': ' '\n' | sed '/^$/d')
|
||||
while IFS= read -r d; do
|
||||
[ -n "$d" ] || continue
|
||||
if [ -d "$d" ] || mkdir -p "$d" 2>/dev/null; then
|
||||
if cp -f "$src_stats" "${d}/stats.json" 2>/dev/null; then
|
||||
chmod a+r "${d}/stats.json" 2>/dev/null || true
|
||||
[ -n "$src_run" ] && [ -f "$src_run" ] && cp -f "$src_run" "${d}/stats-run.json" 2>/dev/null || true
|
||||
log "host: published ${d}/stats.json"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
|
||||
sudo mkdir -p "$d" 2>/dev/null || true
|
||||
sudo cp -f "$src_stats" "${d}/stats.json"
|
||||
sudo chmod a+r "${d}/stats.json" 2>/dev/null || true
|
||||
[ -n "$src_run" ] && [ -f "$src_run" ] && sudo cp -f "$src_run" "${d}/stats-run.json" 2>/dev/null || true
|
||||
log "host: published ${d}/stats.json (sudo)"
|
||||
else
|
||||
log "WARN: cannot write ${d}/stats.json (need write or passwordless sudo)"
|
||||
fi
|
||||
done <<<"$paths"
|
||||
}
|
||||
|
||||
read_pass() {
|
||||
local name="$1" f
|
||||
for f in \
|
||||
|
|
@ -107,12 +175,26 @@ read_pass() {
|
|||
return 0
|
||||
fi
|
||||
done
|
||||
# container (hernani can podman exec root files inside owned containers)
|
||||
if podman inspect -f '{{.State.Running}}' "$BANK_CTR" 2>/dev/null | grep -qx true; then
|
||||
if podman exec "$BANK_CTR" test -r "/root/${name}" 2>/dev/null; then
|
||||
podman exec "$BANK_CTR" cat "/root/${name}" 2>/dev/null | tr -d '\n'
|
||||
# container secrets: /root/… (GOA) or /data/secrets/… (FrancPaysan stage)
|
||||
if [ -n "${BANK_CTR:-}" ] && podman inspect -f '{{.State.Running}}' "$BANK_CTR" 2>/dev/null | grep -qx true; then
|
||||
for path in "/root/${name}" "/data/secrets/${name}" "/data/secrets/bank-admin-password.txt"; do
|
||||
case "$path" in
|
||||
*bank-admin-password.txt)
|
||||
[ "$name" = "bank-admin-password.txt" ] || continue
|
||||
;;
|
||||
esac
|
||||
if podman exec "$BANK_CTR" test -r "$path" 2>/dev/null; then
|
||||
podman exec "$BANK_CTR" cat "$path" 2>/dev/null | tr -d '\n\r'
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
# stage explorer secret naming
|
||||
if [ "$name" = "bank-explorer-password.txt" ]; then
|
||||
if podman exec "$BANK_CTR" test -r /data/secrets/bank-explorer-password.txt 2>/dev/null; then
|
||||
podman exec "$BANK_CTR" cat /data/secrets/bank-explorer-password.txt 2>/dev/null | tr -d '\n\r'
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
|
@ -123,6 +205,8 @@ ctr_running() {
|
|||
|
||||
publish_json() {
|
||||
local ctr="$1" dest_dir="$2" src_stats="$3" src_run="$4"
|
||||
[ "${PUBLISH_PODMAN}" = "1" ] || return 0
|
||||
[ -n "$ctr" ] || return 0
|
||||
if ! ctr_running "$ctr"; then
|
||||
log "WARN: $ctr not running — skip publish $dest_dir"
|
||||
return 1
|
||||
|
|
@ -166,6 +250,7 @@ collect_bank() {
|
|||
BANK_URL="$BANK_URL" \
|
||||
BANK_PUBLIC_URL="$BANK_PUBLIC_URL" \
|
||||
EXCHANGE_CONFIG_URL="$EXCHANGE_CONFIG_URL" \
|
||||
BANK_CURRENCY="${BANK_CURRENCY:-}" \
|
||||
BANK_ADMIN_PASS="$admin_pass" \
|
||||
BANK_EXPLORER_PASS="$explorer_pass" \
|
||||
DEMO_DIR="${demo_dir}" \
|
||||
|
|
@ -173,6 +258,7 @@ collect_bank() {
|
|||
TX_PAGE="${TX_PAGE:-500}" \
|
||||
MAX_TX_PAGES="${MAX_TX_PAGES:-0}" \
|
||||
ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--10000}" \
|
||||
STATS_SOURCE_LABEL="$STATS_SOURCE_LABEL" \
|
||||
"$PY" "$LIB/collect_bank_stats.py" \
|
||||
--out "$WORKDIR/bank-stats.json" \
|
||||
--run-out "$WORKDIR/bank-stats-run.json" \
|
||||
|
|
@ -181,11 +267,13 @@ collect_bank() {
|
|||
set -e
|
||||
|
||||
if [ "$ec_bank" -eq 0 ] && [ -f "$WORKDIR/bank-stats.json" ]; then
|
||||
# Always attach in-container RSS/loadavg (not host /proc)
|
||||
# Optional: attach in-container RSS/loadavg (not host /proc)
|
||||
merge_container_resources bank "$BANK_CTR" "$WORKDIR/bank-stats.json" || true
|
||||
publish_json "$BANK_CTR" "$BANK_LANDING_IN" \
|
||||
"$WORKDIR/bank-stats.json" "$WORKDIR/bank-stats-run.json"
|
||||
log "bank: OK → ${BANK_CTR}:${BANK_LANDING_IN}/stats.json"
|
||||
# Shared public feed: same bank stats on all HOST_STATS_DIRS (bank/ex/merchant landings)
|
||||
publish_host_stats "$WORKDIR/bank-stats.json" "$WORKDIR/bank-stats-run.json"
|
||||
log "bank: OK (podman=${BANK_CTR:-none} host_dirs=${HOST_STATS_DIRS:-none})"
|
||||
else
|
||||
log "ERROR: bank collect failed (ec=$ec_bank) — previous stats.json kept"
|
||||
[ -f "$WORKDIR/bank-stats-run.json" ] || \
|
||||
|
|
@ -272,13 +360,26 @@ collect_merchant() {
|
|||
|
||||
# ---------------------------------------------------------------------------
|
||||
main() {
|
||||
log "=== taler-landing-stats start (user=$(id -un) lib=$LIB) ==="
|
||||
log "=== taler-landing-stats start (user=$(id -un) lib=$LIB currency=${BANK_CURRENCY:-auto}) ==="
|
||||
log "phases bank=${COLLECT_BANK} exchange=${COLLECT_EXCHANGE} merchant=${COLLECT_MERCHANT} resources=${COLLECT_RESOURCES}"
|
||||
if [ "${COLLECT_BANK}" = "1" ]; then
|
||||
collect_bank || ec_bank=$?
|
||||
else
|
||||
log "skip bank collect"
|
||||
fi
|
||||
if [ "${COLLECT_EXCHANGE}" = "1" ]; then
|
||||
collect_exchange || ec_ex=$?
|
||||
else
|
||||
log "skip exchange collect"
|
||||
fi
|
||||
if [ "${COLLECT_MERCHANT}" = "1" ]; then
|
||||
collect_merchant || ec_mer=$?
|
||||
else
|
||||
log "skip merchant collect"
|
||||
fi
|
||||
log "=== done bank=$ec_bank exchange=$ec_ex merchant=$ec_mer ==="
|
||||
# non-zero if bank failed (primary public flow numbers); soft on ex/mer
|
||||
if [ "$ec_bank" -ne 0 ]; then
|
||||
if [ "${COLLECT_BANK}" = "1" ] && [ "$ec_bank" -ne 0 ]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Full-scan bank landing stats (all accounts, all ledger money).
|
||||
|
||||
Run on koopa as hernani (or anywhere with admin API access). Writes stats.json
|
||||
compatible with bank.hacktivism.ch/intro/ plus amount_alt fields for compact UI.
|
||||
Generic for any currency stack (GOA, TESTPAYSAN, KUDOS, …). Run on the ops host
|
||||
(with admin API access). Writes stats.json for public landings + amount_alt.
|
||||
|
||||
Env (selected):
|
||||
BANK_URL default http://127.0.0.1:9012
|
||||
BANK_CURRENCY optional; else from exchange /config or first ledger amount
|
||||
BANK_ADMIN_USER default admin
|
||||
BANK_ADMIN_PASS or BANK_ADMIN_PASS_FILE / pass via --admin-pass-file
|
||||
BANK_EXPLORER_USER default explorer
|
||||
BANK_EXPLORER_PASS optional (balance_explorer)
|
||||
EXCHANGE_CONFIG_URL for alt_unit_names (default https://exchange.hacktivism.ch/config)
|
||||
EXCHANGE_CONFIG_URL for alt_unit_names + currency
|
||||
BANK_PUBLIC_URL public base for latency probes
|
||||
OUT output path (default stdout if -)
|
||||
SCAN_SKIP comma usernames excluded from flow (default: exchange)
|
||||
TX_PAGE page size for transactions delta (default 500)
|
||||
MAX_TX_PAGES 0 = unlimited pages per account (default 0)
|
||||
ACCOUNTS_DELTA GET /accounts?delta= (default -10000)
|
||||
RECENT_WD_N recent withdraws (default 10)
|
||||
TZ default Europe/Zurich
|
||||
TX_PAGE / MAX_TX_PAGES / ACCOUNTS_DELTA / RECENT_WD_N / TZ
|
||||
STATS_SOURCE_LABEL optional string in stats.source field
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -343,6 +342,11 @@ def main() -> int:
|
|||
"--exchange-config",
|
||||
default=env("EXCHANGE_CONFIG_URL", "https://exchange.hacktivism.ch/config"),
|
||||
)
|
||||
ap.add_argument(
|
||||
"--currency",
|
||||
default=env("BANK_CURRENCY", ""),
|
||||
help="ledger currency code (optional; else exchange /config or first amount)",
|
||||
)
|
||||
ap.add_argument("--out", default=env("OUT", "-"))
|
||||
ap.add_argument("--run-out", default=env("RUN_OUT", ""))
|
||||
ap.add_argument(
|
||||
|
|
@ -364,6 +368,11 @@ def main() -> int:
|
|||
)
|
||||
ap.add_argument("--recent", type=int, default=int(env("RECENT_WD_N", "10") or "10"))
|
||||
ap.add_argument("--public-base", default=env("BANK_PUBLIC_URL", "https://bank.hacktivism.ch"))
|
||||
ap.add_argument(
|
||||
"--source-label",
|
||||
default=env("STATS_SOURCE_LABEL", "host collect_bank_stats.py"),
|
||||
help="stats.source field (identify stack / collector)",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
|
||||
tz_name = env("TZ", "Europe/Zurich") or "Europe/Zurich"
|
||||
|
|
@ -378,6 +387,7 @@ def main() -> int:
|
|||
|
||||
bank = args.bank.rstrip("/")
|
||||
skip = {s.strip() for s in (args.skip or "").split(",") if s.strip()}
|
||||
currency_hint = (args.currency or "").strip().upper()
|
||||
|
||||
def write_run(ok: bool, err: Optional[str] = None) -> None:
|
||||
if not args.run_out:
|
||||
|
|
@ -397,6 +407,24 @@ def main() -> int:
|
|||
raise RuntimeError("no admin password (BANK_ADMIN_PASS or --admin-pass-file)")
|
||||
token = get_token(bank, args.admin_user, admin_pass)
|
||||
alt = load_alt_from_config(args.exchange_config)
|
||||
# Currency: explicit → exchange /config → first ledger amount → GOA
|
||||
currency = currency_hint
|
||||
if not currency:
|
||||
try:
|
||||
code_c, cfg_d, _ = http_json(args.exchange_config, timeout=12)
|
||||
if code_c == 200 and isinstance(cfg_d, dict) and cfg_d.get("currency"):
|
||||
currency = str(cfg_d["currency"]).strip().upper()
|
||||
except Exception:
|
||||
pass
|
||||
if not currency:
|
||||
try:
|
||||
code_c, cfg_d, _ = http_json(f"{bank}/config", timeout=12)
|
||||
if code_c == 200 and isinstance(cfg_d, dict) and cfg_d.get("currency"):
|
||||
currency = str(cfg_d["currency"]).strip().upper()
|
||||
except Exception:
|
||||
pass
|
||||
if not currency:
|
||||
currency = "GOA"
|
||||
if not alt:
|
||||
alt = dict(DEFAULT_ALT)
|
||||
|
||||
|
|
@ -442,13 +470,24 @@ def main() -> int:
|
|||
except Exception:
|
||||
continue
|
||||
low = (subject or "").lower()
|
||||
# Normalise bare numbers to stack currency
|
||||
if ":" not in str(amount):
|
||||
amount = f"{currency}:{amount}"
|
||||
else:
|
||||
# adopt ledger currency if we only had a hint
|
||||
try:
|
||||
cur0, _n0 = parse_amount(amount)
|
||||
if cur0 and currency_hint == "" and cur0.upper() != "GOA":
|
||||
currency = cur0.upper()
|
||||
except Exception:
|
||||
pass
|
||||
if direction == "credit":
|
||||
total_in += n
|
||||
n_incoming += 1
|
||||
incomings.append(
|
||||
{
|
||||
"kind": "incoming",
|
||||
"amount": amount if ":" in amount else f"GOA:{amount}",
|
||||
"amount": amount,
|
||||
"at_unix": ts,
|
||||
"account": uname,
|
||||
"subject": subject,
|
||||
|
|
@ -460,7 +499,7 @@ def main() -> int:
|
|||
withdraws.append(
|
||||
{
|
||||
"kind": "withdraw",
|
||||
"amount": amount if ":" in amount else f"GOA:{amount}",
|
||||
"amount": amount,
|
||||
"at_unix": ts,
|
||||
"account": uname,
|
||||
"subject": subject,
|
||||
|
|
@ -503,7 +542,7 @@ def main() -> int:
|
|||
users_n = sum(1 for u in accounts if u not in ("admin", "exchange"))
|
||||
|
||||
def pack_amt(d: Decimal) -> Dict[str, Any]:
|
||||
f = format_amount_alt(f"GOA:{d}", alt)
|
||||
f = format_amount_alt(f"{currency}:{d}", alt)
|
||||
return {
|
||||
"amount": f["amount"],
|
||||
"amount_alt": f["amount_alt"],
|
||||
|
|
@ -567,10 +606,11 @@ def main() -> int:
|
|||
}
|
||||
)
|
||||
|
||||
# explorer balance
|
||||
balance = "GOA:0"
|
||||
bal_alt = "GOA:0"
|
||||
bal_full = "GOA:0"
|
||||
# explorer / pool balance (shared demo account when present)
|
||||
zero = f"{currency}:0"
|
||||
balance = zero
|
||||
bal_alt = zero
|
||||
bal_full = zero
|
||||
if explorer_pass:
|
||||
try:
|
||||
etok = get_token(bank, args.explorer_user, explorer_pass)
|
||||
|
|
@ -583,7 +623,7 @@ def main() -> int:
|
|||
balance = str(
|
||||
data.get("balance", {}).get("amount")
|
||||
if isinstance(data.get("balance"), dict)
|
||||
else data.get("amount") or data.get("balance") or "GOA:0"
|
||||
else data.get("amount") or data.get("balance") or zero
|
||||
)
|
||||
# sometimes balance is object with amount
|
||||
if isinstance(data.get("balance"), dict) and data["balance"].get(
|
||||
|
|
@ -608,7 +648,7 @@ def main() -> int:
|
|||
)
|
||||
if code == 200 and isinstance(data, dict):
|
||||
if isinstance(data.get("balance"), dict):
|
||||
balance = str(data["balance"].get("amount") or "GOA:0")
|
||||
balance = str(data["balance"].get("amount") or zero)
|
||||
elif data.get("amount"):
|
||||
balance = str(data["amount"])
|
||||
bf = format_amount_alt(balance, alt)
|
||||
|
|
@ -637,12 +677,12 @@ def main() -> int:
|
|||
|
||||
stats = {
|
||||
"ok": True,
|
||||
"currency": "GOA",
|
||||
"currency": currency,
|
||||
"timezone": tz_name,
|
||||
"generated_at": gen_iso,
|
||||
"generated_at_human": gen_human,
|
||||
"generated_at_unix": now_u,
|
||||
"source": "host collect_bank_stats.py (hernani systemd)",
|
||||
"source": str(args.source_label or "host collect_bank_stats.py"),
|
||||
"bank_url": bank,
|
||||
"scan": {
|
||||
"tx_page": args.tx_page,
|
||||
|
|
|
|||
|
|
@ -1,35 +1,41 @@
|
|||
#!/usr/bin/env bash
|
||||
# Install central landing-stats collector as hernani user systemd timer.
|
||||
# Install generic landing-stats collector as user systemd timer.
|
||||
#
|
||||
# On koopa:
|
||||
# GOA / koopa (default):
|
||||
# cd ~/src/koopa/koopa-admin-log
|
||||
# ./scripts/taler-landing/install-landing-stats-host.sh
|
||||
# systemctl --user start taler-landing-stats.service # one-shot now
|
||||
# systemctl --user status taler-landing-stats.timer
|
||||
#
|
||||
# Requires: linger enabled for hernani (loginctl enable-linger hernani)
|
||||
# so the timer runs without an interactive login.
|
||||
# Stage TESTPAYSAN (stagepaysan on francpaysan host):
|
||||
# STACK_PROFILE=stage-testpaysan ./scripts/taler-landing/install-landing-stats-host.sh
|
||||
# # or from francpaysan-admin-log: ./scripts/stagepaysan/install-landing-stats.sh
|
||||
#
|
||||
# Optional: STACK_ENV_FILE=/path/to/profile.env
|
||||
# Requires linger: sudo loginctl enable-linger $USER
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
ADMIN_LOG="${ADMIN_LOG:-$ROOT}"
|
||||
SRC="$ADMIN_LOG/scripts/taler-landing"
|
||||
UNIT_SRC="$ADMIN_LOG/configs/systemd/user"
|
||||
PROFILE="${STACK_PROFILE:-goa}"
|
||||
|
||||
BIN_DST="${HOME}/.local/bin"
|
||||
LIB_DST="${HOME}/.local/lib/taler-landing"
|
||||
UNIT_DST="${HOME}/.config/systemd/user"
|
||||
STATE_DST="${HOME}/.local/state/taler-landing-stats"
|
||||
CFG_DST="${HOME}/.config/taler-landing"
|
||||
|
||||
mkdir -p "$BIN_DST" "$LIB_DST" "$UNIT_DST" "$STATE_DST"
|
||||
mkdir -p "$BIN_DST" "$LIB_DST" "$UNIT_DST" "$STATE_DST" "$CFG_DST"
|
||||
|
||||
install -m 0755 "$SRC/collect-landing-stats.sh" "$BIN_DST/collect-landing-stats.sh"
|
||||
install -m 0755 "$SRC/collect_bank_stats.py" "$LIB_DST/collect_bank_stats.py"
|
||||
install -m 0755 "$SRC/enrich_stats_alt.py" "$LIB_DST/enrich_stats_alt.py"
|
||||
install -m 0755 "$SRC/merge_resources.py" "$LIB_DST/merge_resources.py"
|
||||
install -m 0755 "$SRC/collect_container_resources.sh" "$LIB_DST/collect_container_resources.sh"
|
||||
install -m 0755 "$SRC/test-landing-stats.sh" "$BIN_DST/test-landing-stats.sh" 2>/dev/null || \
|
||||
if [ -f "$SRC/test-landing-stats.sh" ]; then
|
||||
install -m 0755 "$SRC/test-landing-stats.sh" "$BIN_DST/test-landing-stats.sh" 2>/dev/null || \
|
||||
install -m 0755 "$SRC/test-landing-stats.sh" "$LIB_DST/test-landing-stats.sh"
|
||||
fi
|
||||
install -m 0644 "$SRC/goa_amounts.py" "$LIB_DST/goa_amounts.py"
|
||||
# mem-snapshot helper lives in taler-shared (copied into containers at collect time)
|
||||
if [ -f "$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh" ]; then
|
||||
|
|
@ -37,18 +43,34 @@ if [ -f "$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh" ]; then
|
|||
"$LIB_DST/mem-snapshot.sh"
|
||||
fi
|
||||
|
||||
# Wrapper always uses installed lib next to itself when LIB discovery works;
|
||||
# also point ADMIN_LOG for in-container script refresh.
|
||||
# Stack profile → ~/.config/taler-landing/stack.env (sourced by collector)
|
||||
if [ -n "${STACK_ENV_FILE:-}" ] && [ -f "${STACK_ENV_FILE}" ]; then
|
||||
install -m 0644 "${STACK_ENV_FILE}" "$CFG_DST/stack.env"
|
||||
elif [ -f "$SRC/profiles/${PROFILE}.env" ]; then
|
||||
install -m 0644 "$SRC/profiles/${PROFILE}.env" "$CFG_DST/stack.env"
|
||||
else
|
||||
echo "WARN: no profile ${PROFILE}.env — collector uses built-in GOA defaults" >&2
|
||||
fi
|
||||
|
||||
install -m 0644 "$UNIT_SRC/taler-landing-stats.service" "$UNIT_DST/"
|
||||
install -m 0644 "$UNIT_SRC/taler-landing-stats.timer" "$UNIT_DST/"
|
||||
|
||||
# Rewrite ExecStart to installed binary if unit uses %h path — units already do.
|
||||
# Point unit EnvironmentFile at stack profile
|
||||
if grep -q '^EnvironmentFile=' "$UNIT_DST/taler-landing-stats.service" 2>/dev/null; then
|
||||
:
|
||||
else
|
||||
# inject after [Service]
|
||||
sed -i '/^\[Service\]/a EnvironmentFile=-%h/.config/taler-landing/stack.env' \
|
||||
"$UNIT_DST/taler-landing-stats.service"
|
||||
fi
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now taler-landing-stats.timer
|
||||
|
||||
echo "Installed (user=$(id -un)):"
|
||||
echo "Installed (user=$(id -un) profile=${PROFILE}):"
|
||||
echo " $BIN_DST/collect-landing-stats.sh"
|
||||
echo " $LIB_DST/{collect_bank_stats,enrich_stats_alt,goa_amounts}.py"
|
||||
echo " $CFG_DST/stack.env"
|
||||
echo " $UNIT_DST/taler-landing-stats.{service,timer} (timer enabled)"
|
||||
echo " logs: $STATE_DST/"
|
||||
echo
|
||||
|
|
@ -61,8 +83,6 @@ echo "Run once now:"
|
|||
echo " systemctl --user start taler-landing-stats.service"
|
||||
echo " journalctl --user -u taler-landing-stats.service -n 40 --no-pager"
|
||||
echo
|
||||
echo "Optional secrets (if not readable via podman exec bank /root/…):"
|
||||
echo " mkdir -p ~/.config/taler-landing"
|
||||
echo " cp …/bank-admin-password.txt ~/.config/taler-landing/"
|
||||
echo " cp …/bank-explorer-password.txt ~/.config/taler-landing/"
|
||||
echo " chmod 600 ~/.config/taler-landing/*"
|
||||
echo "Secrets (if not via podman exec bank):"
|
||||
echo " mkdir -p ~/.config/taler-landing && chmod 700 ~/.config/taler-landing"
|
||||
echo " # bank-admin-password.txt bank-explorer-password.txt"
|
||||
|
|
|
|||
18
scripts/taler-landing/profiles/goa.env
Normal file
18
scripts/taler-landing/profiles/goa.env
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Stack profile: GOA / hacktivism
|
||||
# systemd EnvironmentFile: KEY=value only (no export)
|
||||
BANK_CTR=taler-hacktivism-bank
|
||||
EX_CTR=taler-hacktivism-exchange-ansible
|
||||
MER_CTR=taler-hacktivism
|
||||
BANK_URL=http://127.0.0.1:9012
|
||||
BANK_PUBLIC_URL=https://bank.hacktivism.ch
|
||||
EXCHANGE_CONFIG_URL=https://exchange.hacktivism.ch/config
|
||||
BANK_CURRENCY=GOA
|
||||
BANK_LANDING_IN=/var/www/bank-landing
|
||||
EX_LANDING_IN=/var/www/exchange-landing
|
||||
MER_LANDING_IN=/var/www/merchant-landing
|
||||
COLLECT_BANK=1
|
||||
COLLECT_EXCHANGE=1
|
||||
COLLECT_MERCHANT=1
|
||||
COLLECT_RESOURCES=1
|
||||
PUBLISH_PODMAN=1
|
||||
STATS_SOURCE_LABEL="host collect_bank_stats.py goa hacktivism"
|
||||
22
scripts/taler-landing/profiles/stage-testpaysan.env
Normal file
22
scripts/taler-landing/profiles/stage-testpaysan.env
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Stack profile: FrancPaysan stage / TESTPAYSAN (ops user stagepaysan)
|
||||
# systemd EnvironmentFile: KEY=value only (no export, no shell expansion)
|
||||
BANK_CTR=stage-lfp-bank
|
||||
EX_CTR=stage-lfp-exchange-ansible
|
||||
MER_CTR=stage-lfp-merchant
|
||||
BANK_URL=http://127.0.0.1:9032
|
||||
BANK_PUBLIC_URL=https://stage.bank.lefrancpaysan.ch
|
||||
EXCHANGE_CONFIG_URL=https://stage.exchange.lefrancpaysan.ch/config
|
||||
BANK_CURRENCY=TESTPAYSAN
|
||||
SCAN_SKIP=exchange
|
||||
BANK_LANDING_IN=/data/var/www
|
||||
EX_LANDING_IN=/data/var/www
|
||||
MER_LANDING_IN=/data/var/www
|
||||
# Shared public feed: one bank scan → same stats.json on all three Caddy landings
|
||||
# Colon-separated for systemd EnvironmentFile
|
||||
HOST_STATS_DIRS=/var/www/lfp-stage/bank:/var/www/lfp-stage/exchange:/var/www/lfp-stage/merchant
|
||||
COLLECT_BANK=1
|
||||
COLLECT_EXCHANGE=0
|
||||
COLLECT_MERCHANT=0
|
||||
COLLECT_RESOURCES=0
|
||||
PUBLISH_PODMAN=1
|
||||
STATS_SOURCE_LABEL="host collect_bank_stats.py stage TESTPAYSAN shared"
|
||||
|
|
@ -363,6 +363,7 @@ CUR_JSON="${CUR_JSON}
|
|||
|
||||
GEN=$(now_iso)
|
||||
HUMAN=$(now_human)
|
||||
GEN_UNIX=$(date +%s)
|
||||
|
||||
# Live performance probes (exchange-style)
|
||||
measure_ms() {
|
||||
|
|
@ -406,6 +407,7 @@ cat >"$TMP" <<EOF
|
|||
"timezone": $(json_str "$TZ"),
|
||||
"generated_at": $(json_str "$GEN"),
|
||||
"generated_at_human": $(json_str "$HUMAN"),
|
||||
"generated_at_unix": ${GEN_UNIX},
|
||||
"instances": ${INSTANCES:-0},
|
||||
"orders": ${TOTAL_CONTRACTS:-0},
|
||||
"paid": ${TOTAL_PAID:-0},
|
||||
|
|
|
|||
|
|
@ -1,24 +1,26 @@
|
|||
# taler-monitoring
|
||||
|
||||
Report for the **GOA** stack with clear severity tags and **per-area test IDs**:
|
||||
Report for the **GOA** stack with boxed severity badges and **grouped test IDs**
|
||||
`area.group-NN` (e.g. `www.exchange-01`, `e2e.pay-03`):
|
||||
|
||||
| Tag | Meaning |
|
||||
|-----|---------|
|
||||
| `[OK]` | check passed |
|
||||
| `[INFO]` | inside status (container, ports, log noise) |
|
||||
| `[WARN]` | degraded but maybe not fatal |
|
||||
| `[ERROR]` | component problem |
|
||||
| `[BLOCKER]` | **withdraw/pay path cannot complete** because of this |
|
||||
| `┌ OK ┐` | check passed |
|
||||
| `┌ INFO ┐` | status / note |
|
||||
| `┌ WARN ┐` | degraded but maybe not fatal |
|
||||
| `┌ ERROR ┐` | component problem |
|
||||
| `┌ BLOCKER ┐` | **withdraw/pay path cannot complete** because of this |
|
||||
|
||||
IDs: **`www-001`**, **`inside-001`**, **`versions-001`**, **`sanity-001`**, **`server-001`**, **`e2e-001`** …
|
||||
IDs reset per **group** inside an area — so “too many www tests” become
|
||||
`www.exchange-*`, `www.bank-*`, `www.landing-*`, etc.
|
||||
Catalog: **[TESTS.md](./TESTS.md)**.
|
||||
|
||||
```text
|
||||
[OK] www-001 exchange /config https://exchange…/config
|
||||
[BLOCKER] e2e-015 prereq: merchant HTTP 502
|
||||
┌ OK ┐ www.exchange-01 exchange /config · HTTP 200
|
||||
┌ BLOCKER┐ e2e.prereq-02 merchant HTTP 502
|
||||
```
|
||||
|
||||
End of each phase: totals + list of **BLOCKERS** and **ERRORS**.
|
||||
End of each phase: totals + list of **BLOCKERS** and **ERRORS** (ids included).
|
||||
|
||||
## Commands
|
||||
|
||||
|
|
@ -41,8 +43,27 @@ End of each phase: totals + list of **BLOCKERS** and **ERRORS**.
|
|||
./taler-monitoring.sh --bank https://bank.demo.taler.net \
|
||||
--exchange https://exchange.demo.taler.net \
|
||||
--merchant https://backend.demo.taler.net --currency KUDOS urls
|
||||
|
||||
# Landing stats.json freshness (outside-in; age via generated_at_unix)
|
||||
STATS_STALE_SECS=900 STATS_FAIL_SECS=3600 ./taler-monitoring.sh -d stage.lefrancpaysan.ch urls
|
||||
```
|
||||
|
||||
### Landing `stats.json` (www.stats)
|
||||
|
||||
Public `GET …/intro/stats.json` must expose timestamps so monitoring can tell
|
||||
whether the collector is still updating:
|
||||
|
||||
| Field | Role |
|
||||
|-------|------|
|
||||
| `generated_at_unix` | epoch seconds — **preferred** for age = now − unix |
|
||||
| `generated_at` | ISO-8601 (fallback if unix missing) |
|
||||
| `generated_at_human` | UI “updated” / footer string |
|
||||
|
||||
Also checks display payloads (bank-shape `withdraws` / accounts, or exchange-db /
|
||||
merchant-db counters). Soft WARN if age > `STATS_STALE_SECS` (default 900);
|
||||
ERROR if age > `STATS_FAIL_SECS` (default 3600; set `0` to disable).
|
||||
Stage shared feed: same `gen_unix` on bank + exchange + monnaies.
|
||||
|
||||
## Domain profiles (`domains.conf`)
|
||||
|
||||
Each monitored stack must say **what is bank, exchange, and merchant-backend**.
|
||||
|
|
|
|||
|
|
@ -1,217 +1,132 @@
|
|||
# taler-monitoring — test IDs by area
|
||||
# taler-monitoring — grouped test IDs
|
||||
|
||||
Every check line is numbered **per area** as `AREA-NNN` (zero-padded):
|
||||
|
||||
| Area | Phase script | Meaning |
|
||||
|------|--------------|---------|
|
||||
| **www** | `check_urls.sh` | public HTTPS (outside-in) |
|
||||
| **inside** | `check_inside.sh` | containers / processes on koopa |
|
||||
| **versions** | `check_versions.sh` | deb.taler.net available + packages vs trixie |
|
||||
| **sanity** | `check_sanity.sh` | public + server per component |
|
||||
| **server** | `check_server.sh` | SSH host ports / processes |
|
||||
| **e2e** | `check_e2e.sh` | withdraw + pay cycle |
|
||||
|
||||
Format in output:
|
||||
Every check line is numbered **`area.group-NN`** (two-digit group counter):
|
||||
|
||||
```text
|
||||
[OK] www-001 exchange /config https://exchange…/config
|
||||
[ERROR] e2e-012 bank-auth: admin token failed
|
||||
[BLOCKER] e2e-015 prereq: merchant HTTP 502
|
||||
┌ OK ┐ www.exchange-01 exchange /config · HTTP 200
|
||||
┌ ERROR ┐ e2e.pay-03 order create failed · HTTP 502
|
||||
┌ BLOCKER┐ e2e.prereq-02 merchant secret missing
|
||||
```
|
||||
|
||||
IDs are assigned **in run order** within the area (`set_area` resets the counter). Optional soft checks still consume a number when they WARN.
|
||||
| Area | Phase script | Groups (examples) |
|
||||
|------|--------------|-------------------|
|
||||
| **www** | `check_urls.sh` | `exchange` `perf` `stats` `bank` `merchant` `paivana` `landing` |
|
||||
| **inside** | `check_inside.sh` | `ssh` `bank` `exchange` `merchant` `caddy` `load` |
|
||||
| **versions** | `check_versions.sh` | `outside` `inside` `compare` |
|
||||
| **sanity** | `check_sanity.sh` | `bank` `exchange` `merchant` |
|
||||
| **server** | `check_server.sh` | (flat `server-NN` or host groups) |
|
||||
| **e2e** | `check_e2e.sh` | `prereq` `load` `bank` `wallet` `atm` `settle` `pay` `shop` `paivana` `dig` `report` |
|
||||
| **ladder** | `check_goa_ladder.sh` | `plan` `load` `withdraw` `pay` `report` |
|
||||
|
||||
**Why groups:** flat `www-001`…`www-080` was hard to talk about.
|
||||
`www.bank-04` / `e2e.atm-02` pin the failure to a logical block.
|
||||
|
||||
Usage in scripts:
|
||||
|
||||
```bash
|
||||
set_area www
|
||||
set_group exchange # → www.exchange-01, www.exchange-02, …
|
||||
set_group bank # counter resets → www.bank-01
|
||||
```
|
||||
|
||||
`set_group` also prints a small group chip (`┌ www.exchange ┐`) so logs show section boundaries.
|
||||
Re-entering the same group later (e.g. exchange legal after perf) **continues** NN — `www.exchange-03`, not a second `-01`.
|
||||
Without `set_group`, IDs stay flat: `area-01`, `area-02`, …
|
||||
|
||||
Optional soft checks still consume a number when they WARN.
|
||||
Numbering follows **executed** checks (early skip may shift later NN inside the same group).
|
||||
|
||||
---
|
||||
|
||||
## www — public URLs (`./taler-monitoring.sh urls`)
|
||||
|
||||
| ID | Check |
|
||||
|----|--------|
|
||||
| www-… | exchange `/config`, currency, **alt_unit_names** |
|
||||
| www-… | exchange `/keys` (+ alt_unit_names soft) |
|
||||
| www-… | exchange `/intro/`, `/` (302→intro) |
|
||||
| www-… | **exchange `/terms`** body (not empty / not API error) |
|
||||
| www-… | **exchange `/privacy`** body |
|
||||
| www-… | exchange `/terms/` (200 or redirect) |
|
||||
| www-… | bank `/config`, currency, **alt_unit_names** |
|
||||
| www-… | bank integration / webui / intro / `/` |
|
||||
| www-… | **bank `/terms`** body |
|
||||
| www-… | **bank `/privacy`** (or `/intro/privacy.html` fallback) |
|
||||
| www-… | merchant `/config`, currency, currencies alt_unit_names |
|
||||
| www-… | each merchant `exchanges[]` `/config` alt_unit_names |
|
||||
| www-… | merchant `/intro/`, `/webui/`, `/` |
|
||||
| www-… | **merchant `/terms`** body (dual-currency notice) |
|
||||
| www-… | **merchant `/privacy`** body (must not be `not configured`) |
|
||||
| www-… | merchant `/terms/` redirect |
|
||||
| www-… | **landing (aggregated per site)**: one line for bank/merchant/exchange (`/intro` + assets + own/external link counts); failures listed as samples; soft external/shop assets compact |
|
||||
| www-… | cross-links: one line (4 intros) on local stack |
|
||||
| www-… | bank demo-withdraw + shop assets: one line (+ soft shop-pay if missing) |
|
||||
| www-… | **bank `/intro/demo-withdraw.json`** → `taler://withdraw/HOST:PORT/taler-integration/…` + integration op HTTP 200 |
|
||||
| www-… | **landing exposed links** (bank / merchant / exchange): parse each `/intro/` HTML, probe every own-stack `https://` + root-relative `href`/`src`/`content`, soft-check external stores/docs |
|
||||
| www-… | landing static: `qrcode.min.js`, `og-goa-shop.png`, `qr-logo.png`, shop-pay.js/css |
|
||||
| www-… | cross-links between bank ↔ merchant ↔ exchange intros (local stack) |
|
||||
| www-… | **bank `/intro/demo-withdraw.json`** → `taler://withdraw/HOST/taler-integration/…` (no default `:443`/`:80`; non-default port OK) + integration op HTTP 200 |
|
||||
| www-… | bank `/intro/auto-account.json` (earlier) → same withdraw shape, **no payto_uri**, login at `/webui/` |
|
||||
| www-… | **performance** (outside-in RTT): bank/exchange/merchant `/config`, keys, webui, intro — ms; WARN ≥ `PERF_WARN_MS` (8000); ERROR ≥ `PERF_FAIL_MS` (20000) |
|
||||
| www-… | **landing load stats**: `/intro/stats.json` → loadavg, container RSS, in-container probe ms (public; SSH container cat fallback); warn if stale/missing |
|
||||
| Group | Checks (typical) |
|
||||
|-------|------------------|
|
||||
| **www.exchange-** | `/config`, currency, **alt_unit_names**; `/keys` (+ alt soft); `/intro/`, `/`; **`/terms`**, **`/privacy`**, `/terms/` |
|
||||
| **www.perf-** | outside-in RTT: bank/exchange/merchant `/config`, keys, webui, intro — ms; WARN ≥ `PERF_WARN_MS` (8000); ERROR ≥ `PERF_FAIL_MS` (20000) |
|
||||
| **www.stats-** | `/intro/stats.json` **reachable**; **timestamps** (`generated_at_unix` + human/ISO); **freshness** vs wall clock (`STATS_STALE_SECS` WARN, `STATS_FAIL_SECS` ERROR); **display fields** (bank-shape withdraws/accounts, or exchange-db / merchant-db keys); optional performance block; shared-feed equal `gen_unix` when bank collector is published to all three |
|
||||
| **www.bank-** | `/config`, currency, alt_unit_names; integration/webui/intro; **auto-account.json** (required GOA/local; skip/soft off-GOA e.g. TESTPAYSAN); `/terms`, `/privacy` |
|
||||
| **www.merchant-** | `/config` currency + currencies alt_unit_names; listed exchanges alt; webui/intro; **`/terms`**, **`/privacy`** |
|
||||
| **www.paivana-** | local GOA paywall front (redirect to template) |
|
||||
| **www.landing-** | own-stack intro links; static assets (`qrcode.min.js` hard; `og-goa-shop.png` hard only GOA/local); **demo-withdraw.json** GOA-only; shop-pay soft; cross-links local |
|
||||
|
||||
**Legal docs rule:** HTTP 200, non-empty body, not plain `not configured`, not merchant API JSON `code:21`. On local stack, optional content needle (terms/privacy/FADP/GOA…).
|
||||
**Legal docs rule:** HTTP 200, non-empty body, not plain `not configured`, not merchant API JSON `code:21`. Local stack may require content needle (terms/privacy/FADP/GOA…).
|
||||
|
||||
**Performance rule:** Measured from the **monitoring runner** (public URLs via Caddy), not container loopback. HTTP must match expect (usually 200); **latency (ms) is on every perf OK/WARN/ERROR line**, plus a **perf summary** (n / min / p50 / avg / max). Slow ≥ `PERF_WARN_MS` (default 8000) → WARN; ≥ `PERF_FAIL_MS` (default 20000) → ERROR.
|
||||
**Performance rule:** Measured from the **monitoring runner** (public via Caddy), not container loopback. Latency (ms) on every perf line + **perf summary** (n / min / p50 / avg / max).
|
||||
|
||||
**Landing links rule:** Own-stack (bank/exchange/taler.\* + page host) must be HTTP 200 (or redirect→200). External (App Store, Play, F-Droid, wallet.taler.net, docs/git.taler.net, …) soft WARN if down. Auto-account wallet link must be `taler://withdraw/HOST/taler-integration/…` (default ports stripped for mobile wallets), never payto.
|
||||
**Landing links rule:** Own-stack must be HTTP 200 (or redirect→200). External stores/docs soft WARN. Auto-account wallet link must be `taler://withdraw/HOST/taler-integration/…` (default ports stripped), never payto.
|
||||
|
||||
**alt_unit_names rule:** wallet codec requires a non-empty map including scale key `"0"`. For multi-currency merchant, also follow every entry in `exchanges[]` and check that exchange’s public `/config`.
|
||||
|
||||
(IDs after a failed early check may shift if later soft checks are skipped when body missing — numbering follows **executed** checks.)
|
||||
**alt_unit_names rule:** non-empty map including scale key `"0"`. Multi-currency merchant: follow each `exchanges[]` public `/config`.
|
||||
|
||||
---
|
||||
|
||||
## inside — koopa SSH (`./taler-monitoring.sh inside`)
|
||||
|
||||
| ID | Check (typical order) |
|
||||
|----|------------------------|
|
||||
| inside-001 | ssh koopa (or **koopa-external** fallback) |
|
||||
| inside-002+ | per-component emit: container, ports, libeufin/httpd, postgres, local `/config`/`/keys`, wirewatch, DNS pin, caddy |
|
||||
| inside-… | **load / memory**: host loadavg + RAM; bank/exchange/merchant podman CPU/mem/block + process RSS by role + DB sizes |
|
||||
| Group | Checks |
|
||||
|-------|--------|
|
||||
| **inside.ssh-** | ssh reachability / remote collect |
|
||||
| **inside.bank-** | container, libeufin, postgres, local `/config`, nginx, DNS pins |
|
||||
| **inside.exchange-** | container, httpd, wirewatch, aggregator, transfer, local keys, DNS |
|
||||
| **inside.merchant-** | container, httpd, wirewatch, depositcheck, DNS |
|
||||
| **inside.caddy-** | host reverse-proxy process |
|
||||
| **inside.load-** | host loadavg + RAM; per-container RSS/CPU |
|
||||
|
||||
Remote lines `E|comp|LEVEL|key|detail` each become one numbered result.
|
||||
Remote lines `E|comp|LEVEL|key|detail` each become one numbered result under that component group.
|
||||
|
||||
SSH: `KOOPA_SSH` (default `koopa`), then `KOOPA_SSH_FALLBACKS` (default `koopa-external`) when LAN is unreachable.
|
||||
SSH: `KOOPA_SSH` (default `koopa`), then `KOOPA_SSH_FALLBACKS` when LAN is unreachable.
|
||||
|
||||
---
|
||||
|
||||
## sanity — bank · exchange · merchant (`./taler-monitoring.sh sanity`)
|
||||
## e2e — withdraw + pay (`./taler-monitoring.sh e2e`)
|
||||
|
||||
| ID | Section |
|
||||
|----|---------|
|
||||
| sanity-001… | bank public + server |
|
||||
| sanity-… | exchange public + server |
|
||||
| sanity-… | merchant public + server |
|
||||
| Group | Checks |
|
||||
|-------|--------|
|
||||
| **e2e.prereq-** | wallet-cli, currency, budgets, secrets, public reachability |
|
||||
| **e2e.load-** | host/container load snapshots (before / after ATM / after pay) |
|
||||
| **e2e.bank-** | account, credit, withdraw ops |
|
||||
| **e2e.wallet-** | exchange + ToS, wallet setup |
|
||||
| **e2e.atm-** | ATM withdraw ladder rungs |
|
||||
| **e2e.settle-** | wallet settlement wait / spendable balance |
|
||||
| **e2e.pay-** | variable payments |
|
||||
| **e2e.shop-** | GOA shop product catalog pays |
|
||||
| **e2e.paivana-** | template paywall pay |
|
||||
| **e2e.dig-** | coins-missing dig (inside + withdrawal-operation) |
|
||||
| **e2e.report-** | final tallies |
|
||||
|
||||
Sequential through the whole script (one `set_area sanity`).
|
||||
When filing an issue, quote the full id + label, e.g.
|
||||
`e2e.atm-03 ATM withdraw 5 GOA · bank confirmed, wallet empty`.
|
||||
|
||||
---
|
||||
|
||||
## versions — packages vs deb.taler.net (`./taler-monitoring.sh versions`)
|
||||
## versions / sanity / ladder / server
|
||||
|
||||
### Outside (runner / public network — no SSH)
|
||||
|
||||
| ID (order) | Check |
|
||||
|------------|--------|
|
||||
| versions-… | DNS `deb.taler.net` |
|
||||
| versions-… | HTTPS portal + apt base URL |
|
||||
| versions-… | suite `InRelease` / `Release` |
|
||||
| versions-… | suite `Packages` + `Packages.gz` |
|
||||
| versions-… | sample pool `.deb` fetchable (Range 200/206) |
|
||||
| versions-… | suite offers `taler-exchange`, `taler-merchant`, `libeufin-bank` |
|
||||
| versions-… | optional `trixie-testing` Packages |
|
||||
| versions-… | TLS verify (soft) |
|
||||
|
||||
### Inside (SSH koopa containers)
|
||||
|
||||
| ID | Check |
|
||||
|----|--------|
|
||||
| versions-… | ssh koopa |
|
||||
| versions-… | each container → `InRelease` (pasta can reach apt repo) |
|
||||
| versions-… | each container lists `deb.taler.net` in apt sources |
|
||||
| versions-… | each installed `taler*` / `libeufin*` / `libtaler*` / `libdonau*` vs suite version |
|
||||
| versions-… | core packages installed (`taler-exchange`, `libeufin-bank`, `taler-merchant`) |
|
||||
|
||||
Outside always runs. Inside skipped with `SKIP_SSH=1` (still reports outside results).
|
||||
|
||||
Compare rules:
|
||||
|
||||
- **match** suite → OK
|
||||
- **ahead** of suite (often testing/dev) → INFO
|
||||
- **behind** suite → ERROR for core packages, WARN otherwise (`TALER_PKG_BEHIND=error` forces ERROR)
|
||||
|
||||
Default suite: **trixie** (`TALER_APT_SUITE`, `TALER_APT_BASE=https://deb.taler.net/apt/debian`).
|
||||
|
||||
Without SSH (`SKIP_SSH=1` or remote domain): still runs outside-in repo checks; skips container install compare.
|
||||
| Area.group | Meaning |
|
||||
|------------|---------|
|
||||
| **versions.outside-** | deb.taler.net suite index |
|
||||
| **versions.inside-** | packages in containers |
|
||||
| **versions.compare-** | installed vs suite |
|
||||
| **sanity.bank-** / **.exchange-** / **.merchant-** | public + optional server-side per component |
|
||||
| **ladder.plan-** / **.load-** / **.withdraw-** / **.pay-** / **.report-** | GOA amount ladder |
|
||||
| **server-** | SSH host ports / processes (flat unless grouped later) |
|
||||
|
||||
---
|
||||
|
||||
## server — SSH ports (`./taler-monitoring.sh server`)
|
||||
|
||||
| ID | Check |
|
||||
|----|--------|
|
||||
| server-001 | ssh |
|
||||
| server-002+ | containers, local pasta ports, processes, caddy |
|
||||
|
||||
---
|
||||
|
||||
## e2e — payment path (`./taler-monitoring.sh e2e`)
|
||||
|
||||
Local GOA also: **ATM includes GOA:4200**, then **paivana** (HTTP 302 on `PAIVANA_PUBLIC` + public template pay `paivana` / GOA:4200 on `goa-shop`). Disable with `E2E_PAIVANA=0`.
|
||||
|
||||
| ID | Step (approx.) |
|
||||
|----|----------------|
|
||||
| e2e-001 | budget info |
|
||||
| e2e-002 | wallet-cli present |
|
||||
| e2e-003 | mode / currency info |
|
||||
| e2e-004… | secrets, reachability gates |
|
||||
| e2e-… | account, credit, withdraw, confirm, coins, order, pay ladder |
|
||||
| e2e-… | **load snapshots** at e2e-start, after-withdraw, after-pay, after-shop, e2e-end (host loadavg/RAM + container RSS/CPU/DB) |
|
||||
| e2e-… | **coins** before/after each ATM withdraw, settle, pay, shop: count in circulation / spent, amount + denoms using **`alt_unit_names`** (e.g. `5 Kilo-GOA (GOA:5000)`), Δ vs previous snap |
|
||||
| e2e-… | **GOA shop products** — full catalog list; **random pick of 2** (override `E2E_SHOP_PICK_N`) |
|
||||
| e2e-… | balances, dig on failure, load delta + overall metrics |
|
||||
|
||||
Shop product pays use instance `goa-shop` (default) and catalog `E2E_SHOP_PRODUCTS`
|
||||
(`id|Product name|amount` lines). Each e2e run **shuffles** the catalog and pays
|
||||
only `E2E_SHOP_PICK_N` products (**default 2**). Flow matches the landing popup
|
||||
(public POST `/templates/{id}`, not private orders). Report labels use product name.
|
||||
|
||||
Blockers keep the same ID prefix: `[BLOCKER] e2e-0NN step: message`.
|
||||
|
||||
---
|
||||
|
||||
## ladder — GOA withdraw ranges (`./taler-monitoring.sh ladder`)
|
||||
|
||||
| Step | What |
|
||||
|------|------|
|
||||
| ladder-001 | `GET /intro/auto-account.json` (personal account, GOA:0) |
|
||||
| ladder-… | **load snapshot** before withdraws (host + bank/exchange/merchant mem/CPU) |
|
||||
| ladder-… | **Phase A withdraw** (23): mint explorer pool — 0 → random → max−1 → max into **one cumulative wallet** (mids × `LADDER_WITHDRAW_SCALE`, default 1.5) |
|
||||
| ladder-… | wallet-cli accept-uri + explorer confirm when `selected`; settle; coins snap |
|
||||
| ladder-… | **Phase B pay** (23): same shape 0 → random → max−1 → max via merchant orders + `handle-uri --yes` |
|
||||
| ladder-… | **load snapshot** after + delta + overall timing (withdraw + pay) |
|
||||
| report | withdraw TSV + pay TSV + JSON |
|
||||
|
||||
Default amounts (`build_ladder_pair`, strictly increasing):
|
||||
## Visual format (colour on)
|
||||
|
||||
```text
|
||||
withdraw: [0] + random mids + [max−1] + [max]
|
||||
pay: [0] + (withdraw_mid / scale) + [max−1] + [max] # always ≤ matching withdraw mid
|
||||
╔══════════════════════════════╗
|
||||
║ www · public URLs · … ║
|
||||
╚══════════════════════════════╝
|
||||
┌ www.exchange ┐
|
||||
┌ OK ┐ www.exchange-01 exchange /config · HTTP 200
|
||||
┌ www.bank ┐
|
||||
┌ OK ┐ www.bank-01 bank /config · HTTP 200
|
||||
┌ ERROR ┐ www.bank-04 bank /intro/auto-account.json · invalid withdraw
|
||||
|
||||
┌ ERRORS · failed checks ┐
|
||||
• www.bank-04 bank /intro/auto-account.json · invalid withdraw
|
||||
totals: 40 OK, 1 ERROR, 2 WARN, 5 INFO
|
||||
```
|
||||
|
||||
TSV `range`: `pin:0` | `random` | `pin:max-1` | `pin:max`.
|
||||
|
||||
Soft: absolute **max** mint/pay may `CEILING_REJECT` / skip (WARN). **max-1** is a hard pin.
|
||||
`LADDER_PAY=0` skips phase B.
|
||||
|
||||
| Env | Default | Meaning |
|
||||
|-----|---------|---------|
|
||||
| `LADDER_STEPS` | `23` | total rungs (0 + mids + max) |
|
||||
| `LADDER_MAX_AMOUNT` | `4503599627370496` | absolute last pin |
|
||||
| `LADDER_TIMEOUT_S` | `3600` | large rungs need time |
|
||||
| `LADDER_MAX_RUNGS` | `99` | cap list length after build |
|
||||
|
||||
```bash
|
||||
./taler-monitoring.sh ladder
|
||||
LADDER_STEPS=10 ./taler-monitoring.sh ladder
|
||||
LADDER_REPORT_DIR=/tmp/my-ladder ./taler-monitoring.sh ladder
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Run one area
|
||||
|
||||
```bash
|
||||
./taler-monitoring.sh urls # www only
|
||||
./taler-monitoring.sh inside # inside only
|
||||
./taler-monitoring.sh versions # deb.taler.net + package drift
|
||||
./taler-monitoring.sh e2e # e2e only
|
||||
./taler-monitoring.sh ladder # GOA amount ladder + timings
|
||||
./taler-monitoring.sh -d taler.net urls
|
||||
```
|
||||
`NO_COLOR=1` or `CLICOLOR=0` disables boxes/colours (ASCII `[ OK ]` + `-- group --` headers).
|
||||
Always use `printf --` friendly plain headers when colour is off (leading `---` is not a printf option).
|
||||
|
|
|
|||
|
|
@ -23,19 +23,37 @@ e2e_over() { [ "$(e2e_left)" -le 0 ]; }
|
|||
# stdout only for JSON; logs go to stderr file so balance parse stays clean
|
||||
wcli_bal_snap() {
|
||||
local out="$1"
|
||||
if [ -z "${WALLET_CLI:-}" ] || [ ! -f "${WDB:-}" ]; then
|
||||
if [ -z "${WALLET_CLI:-}" ]; then
|
||||
echo "(no wallet)" >"$out"
|
||||
return 1
|
||||
fi
|
||||
# one-shot needs DB file; serve mode needs socket
|
||||
if [ -z "${WSERVE_PID:-}" ] || [ ! -S "${WSOCK:-}" ]; then
|
||||
if [ ! -f "${WDB:-}" ]; then
|
||||
echo "(no wallet)" >"$out"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
local err="${out}.err"
|
||||
if command -v perl >/dev/null 2>&1; then
|
||||
if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then
|
||||
perl -e 'alarm shift; exec @ARGV' 10 \
|
||||
node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle balance \
|
||||
>"$out" 2>"$err" || true
|
||||
else
|
||||
perl -e 'alarm shift; exec @ARGV' 10 \
|
||||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \
|
||||
>"$out" 2>"$err" || true
|
||||
fi
|
||||
else
|
||||
if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then
|
||||
node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle balance \
|
||||
>"$out" 2>"$err" || true
|
||||
else
|
||||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \
|
||||
>"$out" 2>"$err" || true
|
||||
fi
|
||||
fi
|
||||
[ -s "$out" ]
|
||||
}
|
||||
|
||||
|
|
@ -104,9 +122,9 @@ wait_wallet_balance() {
|
|||
local rounds="${2:-15}"
|
||||
local sleep_s="${3:-3}"
|
||||
local r av
|
||||
info "wallet settle wait" "up to ${rounds}×${sleep_s}s for avail>${min_n} ${CUR} (wirewatch lag is common)"
|
||||
# Poll balance only — never run-until-done (hangs; banned in monitoring).
|
||||
info "wallet settle wait" "up to ${rounds}×${sleep_s}s for avail>${min_n} ${CUR} (poll balance only; no run-until-done)"
|
||||
for r in $(seq 1 "$rounds"); do
|
||||
wcli run-until-done >"$SCRATCH/settle-run.out" 2>&1 || true
|
||||
wcli_bal_snap "$SCRATCH/bal-live.out" || wcli balance >"$SCRATCH/bal-live.out" 2>&1 || true
|
||||
av=$(wallet_avail_num "$SCRATCH/bal-live.out")
|
||||
if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > float(sys.argv[2]) else 1)" "$av" "$min_n" 2>/dev/null; then
|
||||
|
|
@ -182,6 +200,7 @@ e2e_skip_rest() {
|
|||
|
||||
# Area e2e-### — withdraw + pay cycle
|
||||
set_area e2e
|
||||
set_group prereq
|
||||
section "e2e · prerequisites"
|
||||
info "e2e budget" "${E2E_TIMEOUT}s (set E2E_TIMEOUT= to change)"
|
||||
|
||||
|
|
@ -220,12 +239,22 @@ except Exception: print("")' 2>/dev/null || true)
|
|||
if [ "$E2E_REMOTE" = "1" ]; then
|
||||
# remote: still ATM-shaped but smaller notes; keep cheap
|
||||
: "${E2E_WITHDRAW_VALUES:=10 20 50}"
|
||||
if [ "$CUR" = "TESTPAYSAN" ]; then
|
||||
# match fixed public templates e2e-001 / e2e-005 / e2e-1
|
||||
: "${E2E_PAY_VALUES:=0.01 0.05 1}"
|
||||
else
|
||||
: "${E2E_PAY_VALUES:=0.01 0.05 0.1 1}"
|
||||
fi
|
||||
else
|
||||
# local GOA: classic ATM notes + 4200 for paivana paywall template
|
||||
: "${E2E_WITHDRAW_VALUES:=20 50 100 200 4200}"
|
||||
: "${E2E_PAY_VALUES:=0.01 0.05 0.1 0.5 1 2 5 10}"
|
||||
fi
|
||||
# Public template pays (landing/shop style) — default on for TESTPAYSAN stage
|
||||
if [ -z "${E2E_USE_TEMPLATES:-}" ]; then
|
||||
if [ "$CUR" = "TESTPAYSAN" ]; then E2E_USE_TEMPLATES=1; else E2E_USE_TEMPLATES=0; fi
|
||||
fi
|
||||
: "${E2E_USE_TEMPLATES:=0}"
|
||||
|
||||
# Build CUR:amount lists
|
||||
build_amt_list() {
|
||||
|
|
@ -299,12 +328,27 @@ BANK_HOST=$(python3 -c 'from urllib.parse import urlparse; print(urlparse("'"$BA
|
|||
|
||||
SCRATCH=$(mktemp -d)
|
||||
WDB="$SCRATCH/wallet.sqlite3"
|
||||
WSOCK="$SCRATCH/wallet.sock"
|
||||
WSERVE_PID=""
|
||||
# Client/server wallet (advanced serve): long-lived shepherd without run-until-done.
|
||||
# Default on — wallet-cli only completes withdraw/pay while a serve process is up.
|
||||
: "${E2E_WALLET_SERVE:=1}"
|
||||
METRICS_DIR="${METRICS_DIR:-$SCRATCH/metrics}"
|
||||
mkdir -p "$METRICS_DIR"
|
||||
export METRICS_DIR CUR="${CUR:-${EXPECT_CURRENCY:-GOA}}" WDB
|
||||
export PATH="${HOME}/.local/bin:${HOME}/taler/opt/taler-wallet-cli/usr/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
|
||||
|
||||
stop_wallet_serve() {
|
||||
if [ -n "${WSERVE_PID:-}" ] && kill -0 "$WSERVE_PID" 2>/dev/null; then
|
||||
kill "$WSERVE_PID" 2>/dev/null || true
|
||||
wait "$WSERVE_PID" 2>/dev/null || true
|
||||
fi
|
||||
WSERVE_PID=""
|
||||
rm -f "${WSOCK:-}" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Always dump balances on any exit (timeout, blocker, signal, success)
|
||||
trap 'ec=$?; e2e_finish "$ec"; rm -rf "$SCRATCH"; exit "$ec"' EXIT
|
||||
trap 'ec=$?; e2e_finish "$ec"; stop_wallet_serve; rm -rf "$SCRATCH"; exit "$ec"' EXIT
|
||||
|
||||
# Abort cleanly on login / KYC / registration barriers (esp. remote domains)
|
||||
e2e_abort_auth() {
|
||||
|
|
@ -382,10 +426,12 @@ else
|
|||
fi
|
||||
|
||||
# Host + bank/exchange/merchant RAM/CPU/load (SSH koopa or koopa-external)
|
||||
set_group load
|
||||
section "e2e · load snapshot (before withdraw/pay)"
|
||||
metrics_report_load "${METRICS_DIR}/load-before.json" "e2e-start" || true
|
||||
|
||||
# Local stack: koopa secrets. Remote: only explicit env (never leak local passwords to foreign banks).
|
||||
set_group prereq
|
||||
if [ "$E2E_REMOTE" = "1" ]; then
|
||||
ADMIN_PASS="${E2E_BANK_ADMIN_PASS:-}"
|
||||
MPW="${E2E_MERCHANT_TOKEN:-${MERCHANT_TOKEN:-}}"
|
||||
|
|
@ -402,13 +448,29 @@ else
|
|||
exit 1
|
||||
fi
|
||||
if [ -n "$MPW" ]; then
|
||||
# Secrets often store full "secret-token:…"; strip before re-prefixing Bearer.
|
||||
MPW=$(printf '%s' "$MPW" | tr -d '\n\r')
|
||||
case "$MPW" in
|
||||
secret-token:*) MPW_TOKEN="$MPW" ;;
|
||||
*) MPW_TOKEN="secret-token:${MPW}" ;;
|
||||
esac
|
||||
ok "merchant secret (instance ${MERCHANT_INSTANCE})"
|
||||
elif [ "$E2E_REMOTE" = "1" ]; then
|
||||
warn "merchant secret" "missing — order create may fail (set E2E_MERCHANT_TOKEN)"
|
||||
MPW_TOKEN=""
|
||||
warn "merchant secret" "missing — private orders fail unless E2E_USE_TEMPLATES=1 (set E2E_MERCHANT_TOKEN)"
|
||||
else
|
||||
blocker "prereq" "merchant instance password missing"
|
||||
exit 1
|
||||
fi
|
||||
# Authorization header for merchant private API (never double secret-token:)
|
||||
merchant_auth_header() {
|
||||
local t="${1:-${MPW_TOKEN:-}}"
|
||||
[ -n "$t" ] || return 1
|
||||
case "$t" in
|
||||
secret-token:*) printf 'Authorization: Bearer %s' "$t" ;;
|
||||
*) printf 'Authorization: Bearer secret-token:%s' "$t" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Public reachability gates (remote: soft-skip if bank/merchant absent)
|
||||
for pair in \
|
||||
|
|
@ -452,7 +514,31 @@ info "e2e user" "$USER withdraw=$WITHDRAW_AMT pay=$PAY_AMT"
|
|||
BANK="$BANK_PUBLIC"
|
||||
INST="$MERCHANT_INSTANCE"
|
||||
|
||||
start_wallet_serve() {
|
||||
[ "${E2E_WALLET_SERVE}" = "1" ] || return 0
|
||||
[ -n "${WALLET_CLI:-}" ] && [ -f "${WALLET_CLI}" ] || return 1
|
||||
stop_wallet_serve
|
||||
rm -f "$WSOCK"
|
||||
# serve holds the DB open; clients use --wallet-connection only
|
||||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults \
|
||||
advanced serve --unix-path "$WSOCK" \
|
||||
>"$SCRATCH/wallet-serve.log" 2>&1 &
|
||||
WSERVE_PID=$!
|
||||
local i
|
||||
for i in $(seq 1 40); do
|
||||
if [ -S "$WSOCK" ] && kill -0 "$WSERVE_PID" 2>/dev/null; then
|
||||
ok "wallet serve" "pid=$WSERVE_PID sock=$WSOCK (no run-until-done)"
|
||||
return 0
|
||||
fi
|
||||
sleep 0.15
|
||||
done
|
||||
warn "wallet serve" "socket not ready — falling back to one-shot CLI (coins may stay pending)"
|
||||
stop_wallet_serve
|
||||
return 1
|
||||
}
|
||||
|
||||
# $1 = max seconds for this call (optional); rest = wallet-cli args
|
||||
# Prefer live serve socket so the shepherd stays up (alternative to run-until-done).
|
||||
wcli() {
|
||||
local maxc=12
|
||||
if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
|
||||
|
|
@ -464,12 +550,21 @@ wcli() {
|
|||
cap=$(e2e_left)
|
||||
[ "$cap" -gt "$maxc" ] && cap=$maxc
|
||||
[ "$cap" -lt 3 ] && return 124
|
||||
if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then
|
||||
if command -v perl >/dev/null 2>&1; then
|
||||
perl -e 'alarm shift; exec @ARGV' "$cap" \
|
||||
node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@"
|
||||
else
|
||||
node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@"
|
||||
fi
|
||||
else
|
||||
if command -v perl >/dev/null 2>&1; then
|
||||
perl -e 'alarm shift; exec @ARGV' "$cap" \
|
||||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
|
||||
else
|
||||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Pay must not be starved: use reserved seconds even if global budget is tight
|
||||
|
|
@ -486,16 +581,26 @@ wcli_pay() {
|
|||
cap=12
|
||||
fi
|
||||
info "pay wallet cap" "${cap}s"
|
||||
if [ -n "${WSERVE_PID:-}" ] && [ -S "${WSOCK:-}" ]; then
|
||||
if command -v perl >/dev/null 2>&1; then
|
||||
perl -e 'alarm shift; exec @ARGV' "$cap" \
|
||||
node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@"
|
||||
else
|
||||
node "$WALLET_CLI" --wallet-connection="$WSOCK" --no-throttle "$@"
|
||||
fi
|
||||
else
|
||||
if command -v perl >/dev/null 2>&1; then
|
||||
perl -e 'alarm shift; exec @ARGV' "$cap" \
|
||||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
|
||||
else
|
||||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# When coins never become available: run inside + targeted bank/exchange dig
|
||||
dig_when_no_coins() {
|
||||
set_group dig
|
||||
section "e2e · coins missing — inside dig"
|
||||
info "reason" "wallet empty after withdraw path (often wirewatch timing) — probing bank + exchange"
|
||||
if [ -n "${WID:-}" ]; then
|
||||
|
|
@ -580,6 +685,7 @@ BAL_BEFORE=$(fmt_bal "$SCRATCH/bal-before.out")
|
|||
info "BALANCE before" "$BAL_BEFORE"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
set_group bank
|
||||
section "e2e · bank (account · credit · withdraw)"
|
||||
# ---------------------------------------------------------------------------
|
||||
AT=""
|
||||
|
|
@ -678,8 +784,11 @@ else
|
|||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
set_group wallet
|
||||
section "e2e · wallet setup (exchange + ToS once)"
|
||||
# ---------------------------------------------------------------------------
|
||||
# Long-lived serve = client/server wallet (shepherd keeps running; no run-until-done).
|
||||
start_wallet_serve || true
|
||||
if ! wcli exchanges add "${EXCHANGE_PUBLIC}/" >"$SCRATCH/ex-add.out" 2>&1; then
|
||||
if ! grep -qiE 'already|ok' "$SCRATCH/ex-add.out"; then
|
||||
blocker "wallet-exchange" "exchanges add failed — $(tail -c 160 "$SCRATCH/ex-add.out" | tr '\n' ' ')"
|
||||
|
|
@ -774,12 +883,13 @@ e2e_one_withdraw() {
|
|||
fi
|
||||
cp "$SCRATCH/accept-$tag.out" "$SCRATCH/accept.out"
|
||||
|
||||
# Poll bank withdrawal status only (no run-until-done). force-select if stuck pending.
|
||||
st=""
|
||||
for i in 1 2 3 4; do
|
||||
for i in 1 2 3 4 5 6 8 10; do
|
||||
e2e_over && break
|
||||
wcli run-until-done >"$SCRATCH/sel-$tag-$i.out" 2>&1 || true
|
||||
st=$(wd_status)
|
||||
case "$st" in selected|confirmed|aborted) break ;; esac
|
||||
sleep 0.4
|
||||
done
|
||||
if [ "$st" != "selected" ] && [ "$st" != "confirmed" ]; then
|
||||
wcli transactions >"$SCRATCH/tx-pre.json" 2>&1 || true
|
||||
|
|
@ -824,10 +934,9 @@ else:
|
|||
fi
|
||||
fake_incoming_speedup
|
||||
|
||||
# Short per-ATM poll; full settle wait happens after the ladder (avoids false FAIL)
|
||||
local ok_bal=0 r av
|
||||
for r in 1 2 3 4 5 6; do
|
||||
wcli run-until-done >"$SCRATCH/run-$tag-$r.out" 2>&1 || true
|
||||
# Short per-ATM settle: poll balance + bank transfer_done only (never run-until-done)
|
||||
local ok_bal=0 r av xfer
|
||||
for r in 1 2 3 4 5 6 8 10; do
|
||||
wcli_bal_snap "$SCRATCH/bal-$tag.out" || wcli balance >"$SCRATCH/bal-$tag.out" 2>&1 || true
|
||||
av=$(wallet_avail_num "$SCRATCH/bal-$tag.out")
|
||||
if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > 0 else 1)" "$av" 2>/dev/null; then
|
||||
|
|
@ -836,6 +945,13 @@ else:
|
|||
info "balance" "$(fmt_bal "$SCRATCH/bal-$tag.out")"
|
||||
break
|
||||
fi
|
||||
xfer=$(curl -sS -m 8 "$BANK/taler-integration/withdrawal-operation/${WID}" 2>/dev/null \
|
||||
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("transfer_done"), d.get("status"))' 2>/dev/null || echo "?")
|
||||
if echo "$xfer" | grep -qi True; then
|
||||
ok "ATM $WITHDRAW_AMT bank transfer_done" "wallet avail=${CUR}:${av} ($xfer) — no run-until-done"
|
||||
ok_bal=1
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
metrics_report_coins "after-ATM-${tag}" || true
|
||||
|
|
@ -853,8 +969,28 @@ else:
|
|||
return 1
|
||||
}
|
||||
|
||||
# Map CUR:amount → public template id (TESTPAYSAN stage e2e-* templates).
|
||||
# Override with E2E_TEMPLATE_MAP="0.01=e2e-001 0.05=e2e-005 1=e2e-1"
|
||||
e2e_template_id_for_amount() {
|
||||
local amt="$1"
|
||||
local val map_entry k v
|
||||
val=$(printf '%s' "$amt" | awk -F: '{print $NF}')
|
||||
: "${E2E_TEMPLATE_MAP:=0.01=e2e-001 0.05=e2e-005 1=e2e-1 1.0=e2e-1}"
|
||||
for map_entry in $E2E_TEMPLATE_MAP; do
|
||||
k="${map_entry%%=*}"
|
||||
v="${map_entry#*=}"
|
||||
if [ "$k" = "$val" ]; then
|
||||
printf '%s' "$v"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# One payment: e2e_one_pay AMOUNT [SUMMARY] [TAG]
|
||||
# SUMMARY defaults to "monitoring pay $AMOUNT"; TAG defaults from amount.
|
||||
# When E2E_USE_TEMPLATES=1 and amount maps to a public template, uses
|
||||
# POST /instances/{inst}/templates/{id} (same as landings/shops).
|
||||
e2e_one_pay() {
|
||||
PAY_AMT="$1"
|
||||
local PAY_SUM="${2:-monitoring pay ${PAY_AMT}}"
|
||||
|
|
@ -867,11 +1003,28 @@ e2e_one_pay() {
|
|||
section "e2e · pay $PAY_AMT · $(format_amount_alt "$PAY_AMT") · $PAY_SUM"
|
||||
e2e_over && { warn "pay" "budget exhausted — skip $PAY_AMT ($PAY_SUM)"; return 1; }
|
||||
metrics_report_coins "before-pay-${tag}" || true
|
||||
if [ -z "${MPW:-}" ]; then
|
||||
|
||||
# Prefer public templates when enabled (stage TESTPAYSAN / shop-style orders)
|
||||
local tpl_id=""
|
||||
if [ "${E2E_USE_TEMPLATES:-0}" = "1" ]; then
|
||||
tpl_id=$(e2e_template_id_for_amount "$PAY_AMT" 2>/dev/null || true)
|
||||
if [ -n "$tpl_id" ]; then
|
||||
info "pay $PAY_AMT" "via public template $tpl_id (E2E_USE_TEMPLATES=1)"
|
||||
e2e_one_pay_public_template "$tpl_id" "$PAY_SUM" "$PAY_AMT"
|
||||
return $?
|
||||
fi
|
||||
warn "pay $PAY_AMT" "no template map for amount — fall back to private order"
|
||||
fi
|
||||
|
||||
if [ -z "${MPW:-}" ] && [ -z "${MPW_TOKEN:-}" ]; then
|
||||
warn "pay $PAY_AMT" "no merchant token"
|
||||
return 1
|
||||
fi
|
||||
AUTH="Authorization: Bearer secret-token:${MPW}"
|
||||
local AUTH
|
||||
AUTH=$(merchant_auth_header) || {
|
||||
warn "pay $PAY_AMT" "no merchant token"
|
||||
return 1
|
||||
}
|
||||
# JSON-escape summary for curl -d
|
||||
local SUM_JSON
|
||||
SUM_JSON=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$PAY_SUM" 2>/dev/null || printf '"%s"' "$PAY_SUM")
|
||||
|
|
@ -881,8 +1034,9 @@ e2e_one_pay() {
|
|||
"${MERCHANT_PUBLIC}/instances/${INST}/private/orders" 2>"$SCRATCH/ord-$tag.err" || true
|
||||
if ! grep -q order_id "$SCRATCH/ord-$tag.json" 2>/dev/null; then
|
||||
if [ "$E2E_REMOTE" != "1" ] && koopa_ssh_ok; then
|
||||
local mpw_raw="${MPW_TOKEN:-secret-token:${MPW}}"
|
||||
koopa_ssh_run 20 \
|
||||
"curl -skS -m 12 -X POST -H 'Authorization: Bearer secret-token:${MPW}' -H 'Content-Type: application/json' -d '{\"order\":{\"summary\":${SUM_JSON},\"amount\":\"${PAY_AMT}\",\"fulfillment_message\":\"ok\"},\"create_token\":true}' 'https://127.0.0.1:9010/instances/${INST}/private/orders'" \
|
||||
"curl -skS -m 12 -X POST -H 'Authorization: Bearer ${mpw_raw}' -H 'Content-Type: application/json' -d '{\"order\":{\"summary\":${SUM_JSON},\"amount\":\"${PAY_AMT}\",\"fulfillment_message\":\"ok\"},\"create_token\":true}' 'https://127.0.0.1:9010/instances/${INST}/private/orders'" \
|
||||
>"$SCRATCH/ord-$tag.json" 2>"$SCRATCH/ord-$tag.err" || true
|
||||
fi
|
||||
fi
|
||||
|
|
@ -926,23 +1080,37 @@ except Exception: print("")
|
|||
return 1
|
||||
fi
|
||||
ok "wallet handle pay $PAY_AMT ($PAY_SUM)"
|
||||
# Settle: poll merchant order + wallet transactions only (never run-until-done)
|
||||
local r
|
||||
for r in 1 2 3; do
|
||||
wcli_pay run-until-done >"$SCRATCH/pay-run-$tag.out" 2>&1 || true
|
||||
for r in 1 2 3 4 5 6; do
|
||||
wcli_pay transactions >"$SCRATCH/tx-$tag.out" 2>&1 || true
|
||||
if [ -n "${AUTH:-}" ]; then
|
||||
curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \
|
||||
"${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "${OTOK:-}" ]; then
|
||||
curl -skS -m 8 -o "$SCRATCH/ord-pub-$tag.json" \
|
||||
"${MERCHANT_PUBLIC}/instances/${INST}/orders/$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$OID")?token=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$OTOK")" \
|
||||
2>/dev/null || true
|
||||
fi
|
||||
if grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$tag.out" 2>/dev/null \
|
||||
|| grep -qiE 'done|paid|success|Payment' "$SCRATCH/pay-$tag.out" 2>/dev/null \
|
||||
|| python3 -c 'import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid" else 1)
|
||||
' "$SCRATCH/ord-paid-$tag.json" 2>/dev/null; then
|
||||
for p in sys.argv[1:]:
|
||||
try:
|
||||
d=json.load(open(p))
|
||||
if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid":
|
||||
sys.exit(0)
|
||||
except Exception:
|
||||
pass
|
||||
sys.exit(1)
|
||||
' "$SCRATCH/ord-paid-$tag.json" "$SCRATCH/ord-pub-$tag.json" 2>/dev/null; then
|
||||
ok "payment settled $PAY_AMT ($PAY_SUM · order $OID)"
|
||||
metrics_report_coins "after-pay-${tag}" || true
|
||||
metrics_record_flow spent "$PAY_AMT" || true
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
metrics_report_coins "after-pay-fail-${tag}" || true
|
||||
warn "pay $PAY_AMT ($PAY_SUM)" "not settled for order $OID"
|
||||
|
|
@ -1045,33 +1213,34 @@ except Exception: print("")
|
|||
fi
|
||||
ok "shop $pname" "wallet accepted pay URI"
|
||||
|
||||
# Settle: public order status + transactions only (never run-until-done)
|
||||
local r AUTH=""
|
||||
if [ -n "${MPW:-}" ]; then
|
||||
AUTH="Authorization: Bearer secret-token:${MPW}"
|
||||
fi
|
||||
for r in 1 2 3; do
|
||||
wcli_pay run-until-done >"$SCRATCH/pay-run-$tag.out" 2>&1 || true
|
||||
AUTH=$(merchant_auth_header 2>/dev/null) || AUTH=""
|
||||
for r in 1 2 3 4 5 6; do
|
||||
wcli_pay transactions >"$SCRATCH/tx-$tag.out" 2>&1 || true
|
||||
if [ -n "$AUTH" ]; then
|
||||
curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \
|
||||
"${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
|
||||
fi
|
||||
curl -skS -m 8 -o "$SCRATCH/ord-pub-$tag.json" "$statusUrl" 2>/dev/null || true
|
||||
if python3 -c 'import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid" else 1)
|
||||
' "$SCRATCH/ord-paid-$tag.json" 2>/dev/null; then
|
||||
for p in sys.argv[1:]:
|
||||
try:
|
||||
d=json.load(open(p))
|
||||
if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid":
|
||||
sys.exit(0)
|
||||
except Exception:
|
||||
pass
|
||||
sys.exit(1)
|
||||
' "$SCRATCH/ord-paid-$tag.json" "$SCRATCH/ord-pub-$tag.json" 2>/dev/null \
|
||||
|| grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$tag.out" 2>/dev/null \
|
||||
|| grep -qiE 'done|paid|success|Payment' "$SCRATCH/pay-$tag.out" 2>/dev/null; then
|
||||
ok "shop $pname" "payment settled ($pamt · order $OID)"
|
||||
metrics_report_coins "after-shop-${tag}" || true
|
||||
metrics_record_flow spent "$pamt" || true
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
if grep -qiE 'payment|paid|Payment' "$SCRATCH/tx-$tag.out" 2>/dev/null \
|
||||
|| grep -qiE 'done|paid|success|Payment' "$SCRATCH/pay-$tag.out" 2>/dev/null; then
|
||||
ok "shop $pname" "payment settled via wallet tx ($pamt · order $OID)"
|
||||
metrics_report_coins "after-shop-${tag}" || true
|
||||
metrics_record_flow spent "$pamt" || true
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
metrics_report_coins "after-shop-fail-${tag}" || true
|
||||
warn "shop $pname ($pid)" "not settled for order $OID"
|
||||
|
|
@ -1079,6 +1248,7 @@ sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="p
|
|||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
set_group atm
|
||||
section "e2e · ATM withdraw ladder"
|
||||
# ---------------------------------------------------------------------------
|
||||
WITHDRAW_OK=0
|
||||
|
|
@ -1109,12 +1279,14 @@ for WITHDRAW_AMT in $WITHDRAW_LIST; do
|
|||
esac
|
||||
done
|
||||
info "ATM withdraw summary" "$WITHDRAW_REPORT (ok=$WITHDRAW_OK_N lag=$WITHDRAW_LAG_N fail=$WITHDRAW_FAIL_N)"
|
||||
set_group load
|
||||
section "e2e · load snapshot (after ATM withdraws)"
|
||||
metrics_report_load "${METRICS_DIR}/load-after-withdraw.json" "after-withdraw" || true
|
||||
cp -f "${METRICS_DIR}/load-after-withdraw.json" "${METRICS_DIR}/load-after.json" 2>/dev/null || true
|
||||
metrics_report_coins "after-ATM-ladder" || true
|
||||
|
||||
# Settlement catch-up: bank may have confirmed while wallet was still empty
|
||||
set_group settle
|
||||
section "e2e · wallet settlement (timing)"
|
||||
if ! wait_wallet_balance 0 "${E2E_SETTLE_ROUNDS}" "${E2E_SETTLE_SLEEP}"; then
|
||||
if [ "$WITHDRAW_LAG_N" -gt 0 ] || [ "$WITHDRAW_OK_N" -gt 0 ]; then
|
||||
|
|
@ -1154,6 +1326,7 @@ else
|
|||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
set_group pay
|
||||
section "e2e · variable payments (if balance allows)"
|
||||
# ---------------------------------------------------------------------------
|
||||
PAY_OK=0
|
||||
|
|
@ -1161,13 +1334,16 @@ PAY_OK_N=0
|
|||
PAY_FAIL_N=0
|
||||
PAY_SKIP_N=0
|
||||
PAY_REPORT=""
|
||||
if [ -z "${MPW:-}" ]; then
|
||||
if [ -z "${MPW:-}" ] && [ "${E2E_USE_TEMPLATES:-0}" != "1" ]; then
|
||||
if [ "$E2E_REMOTE" = "1" ]; then
|
||||
e2e_abort_auth "merchant-order" "no merchant token — set E2E_MERCHANT_TOKEN"
|
||||
e2e_abort_auth "merchant-order" "no merchant token — set E2E_MERCHANT_TOKEN (or E2E_USE_TEMPLATES=1)"
|
||||
fi
|
||||
blocker "merchant-order" "no merchant token"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${MPW:-}" ] && [ "${E2E_USE_TEMPLATES:-0}" = "1" ]; then
|
||||
info "pay" "no merchant token — public templates only (E2E_USE_TEMPLATES=1)"
|
||||
fi
|
||||
|
||||
for PAY_AMT in $PAY_LIST; do
|
||||
e2e_over && { warn "pay ladder" "time budget low — stopping more payments"; break; }
|
||||
|
|
@ -1193,6 +1369,7 @@ for PAY_AMT in $PAY_LIST; do
|
|||
fi
|
||||
done
|
||||
info "pay summary" "$PAY_REPORT (ok=$PAY_OK_N fail=$PAY_FAIL_N skip=$PAY_SKIP_N)"
|
||||
set_group load
|
||||
section "e2e · load snapshot (after payments)"
|
||||
metrics_report_load "${METRICS_DIR}/load-after-pay.json" "after-pay" || true
|
||||
cp -f "${METRICS_DIR}/load-after-pay.json" "${METRICS_DIR}/load-after.json" 2>/dev/null || true
|
||||
|
|
@ -1207,18 +1384,47 @@ if [ "$PAY_OK" != "1" ]; then
|
|||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "e2e · GOA shop products (merchant landing catalog)"
|
||||
set_group shop
|
||||
section "e2e · shop products (public templates)"
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public template POST + wallet pay — full catalog list, random pick of N products.
|
||||
# Public template POST + wallet pay — catalog random pick of N products.
|
||||
# GOA: goa-shop on hacktivism. TESTPAYSAN: farmer shops (jardin / fermes).
|
||||
SHOP_OK=0
|
||||
SHOP_OK_N=0
|
||||
SHOP_FAIL_N=0
|
||||
SHOP_SKIP_N=0
|
||||
SHOP_REPORT=""
|
||||
if [ "$E2E_REMOTE" = "1" ] || [ "${CUR:-}" != "GOA" ]; then
|
||||
info "goa-shop" "SKIPPED (remote or non-GOA currency)"
|
||||
# Auto catalog for stage TESTPAYSAN when not overridden
|
||||
if [ "${CUR:-}" = "TESTPAYSAN" ] && [ -z "${E2E_SHOP_PRODUCTS_SET:-}" ] && [ -z "${E2E_SHOP_FORCE_GOA:-}" ]; then
|
||||
# id|name|amount|instance — force stage catalog (GOA default is set earlier)
|
||||
E2E_SHOP_PRODUCTS="oeufs-6|Œufs plein air × 6|TESTPAYSAN:5|jardin-du-creux
|
||||
pain-seigle|Pain de seigle 800 g|TESTPAYSAN:4.5|jardin-du-creux
|
||||
jus-pomme|Jus de pomme 1 L|TESTPAYSAN:6|jardin-du-creux
|
||||
panier-legumes|Panier légumes|TESTPAYSAN:25|fermes-des-collines
|
||||
fromage-chevre|Fromage de chèvre 200 g|TESTPAYSAN:8.5|fermes-des-collines
|
||||
miel-printemps|Miel de printemps 250 g|TESTPAYSAN:12|fermes-des-collines"
|
||||
# Force stage defaults (GOA goa-shop is already set earlier via :=)
|
||||
if [ -z "${E2E_SHOP_INSTANCE_SET:-}" ]; then
|
||||
E2E_SHOP_INSTANCE=jardin-du-creux
|
||||
fi
|
||||
if [ -z "${E2E_SHOP_PAYTO_SET:-}" ]; then
|
||||
E2E_SHOP_PAYTO="payto://x-taler-bank/stage.bank.lefrancpaysan.ch/jardin-du-creux"
|
||||
fi
|
||||
E2E_SHOP_PICK_N="${E2E_SHOP_PICK_N:-2}"
|
||||
E2E_SHOP_ENABLE=1
|
||||
fi
|
||||
# GOA local only by default; remote non-GOA shops need E2E_SHOP_ENABLE=1 or TESTPAYSAN auto above
|
||||
if [ -z "${E2E_SHOP_ENABLE:-}" ]; then
|
||||
if [ "$E2E_REMOTE" = "1" ] || [ "${CUR:-}" != "GOA" ]; then
|
||||
E2E_SHOP_ENABLE=0
|
||||
else
|
||||
E2E_SHOP_ENABLE=1
|
||||
fi
|
||||
fi
|
||||
if [ "${E2E_SHOP_ENABLE}" != "1" ]; then
|
||||
info "shop" "SKIPPED (set E2E_SHOP_ENABLE=1 or use TESTPAYSAN stage catalog)"
|
||||
else
|
||||
# Build catalog array from E2E_SHOP_PRODUCTS (id|name|amount lines)
|
||||
# Build catalog array from E2E_SHOP_PRODUCTS (id|name|amount[|instance] lines)
|
||||
SHOP_CATALOG=()
|
||||
while IFS= read -r line; do
|
||||
[ -z "$line" ] && continue
|
||||
|
|
@ -1232,7 +1438,7 @@ EOF
|
|||
case "$PICK_N" in ''|*[!0-9]*) PICK_N=2 ;; esac
|
||||
if [ "$PICK_N" -lt 1 ]; then PICK_N=1; fi
|
||||
if [ "$SHOP_CAT_N" -eq 0 ]; then
|
||||
warn "goa-shop" "empty E2E_SHOP_PRODUCTS catalog"
|
||||
warn "shop" "empty E2E_SHOP_PRODUCTS catalog"
|
||||
else
|
||||
if [ "$PICK_N" -gt "$SHOP_CAT_N" ]; then PICK_N=$SHOP_CAT_N; fi
|
||||
# Shuffle catalog, take first PICK_N (portable: python)
|
||||
|
|
@ -1245,24 +1451,26 @@ random.shuffle(lines)
|
|||
print('\n'.join(lines[:n]))
|
||||
" "$PICK_N"
|
||||
)
|
||||
info "goa-shop" "catalog ${SHOP_CAT_N} products · random pick ${PICK_N} · instance ${E2E_SHOP_INSTANCE}"
|
||||
info "goa-shop payto" "$E2E_SHOP_PAYTO"
|
||||
info "goa-shop pick" "$(printf '%s\n' "$SHOP_PICKED" | cut -d'|' -f1,2 | tr '\n' '; ' | sed 's/; $//')"
|
||||
# e2e_one_pay_public_template uses global INST — temporarily pin shop instance
|
||||
info "shop" "catalog ${SHOP_CAT_N} products · random pick ${PICK_N} · default instance ${E2E_SHOP_INSTANCE}"
|
||||
info "shop payto" "$E2E_SHOP_PAYTO"
|
||||
info "shop pick" "$(printf '%s\n' "$SHOP_PICKED" | cut -d'|' -f1,2 | tr '\n' '; ' | sed 's/; $//')"
|
||||
# e2e_one_pay_public_template uses global INST — pin per product if 4th field set
|
||||
_E2E_INST_SAVE="$INST"
|
||||
INST="${E2E_SHOP_INSTANCE}"
|
||||
while IFS= read -r line; do
|
||||
[ -z "$line" ] && continue
|
||||
e2e_over && { warn "goa-shop" "time budget low — stopping product pays"; break; }
|
||||
e2e_over && { warn "shop" "time budget low — stopping product pays"; break; }
|
||||
pid=$(printf '%s' "$line" | cut -d'|' -f1)
|
||||
pname=$(printf '%s' "$line" | cut -d'|' -f2)
|
||||
pamt=$(printf '%s' "$line" | cut -d'|' -f3)
|
||||
pinst=$(printf '%s' "$line" | cut -d'|' -f4)
|
||||
[ -z "$pid" ] || [ -z "$pamt" ] && continue
|
||||
[ -z "$pname" ] && pname="$pid"
|
||||
INST="${pinst:-${E2E_SHOP_INSTANCE}}"
|
||||
av_now=$(wallet_avail_num)
|
||||
pay_n=$(python3 -c 'import sys; print(float(sys.argv[1].split(":",1)[-1]))' "$pamt" 2>/dev/null || echo 0)
|
||||
if ! python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) + 1e-12 >= float(sys.argv[2]) else 1)" "$av_now" "$pay_n" 2>/dev/null; then
|
||||
warn "goa-shop $pname ($pid)" "skip — insufficient avail ${CUR}:${av_now} (need ${pay_n})"
|
||||
warn "shop $pname ($pid)" "skip — insufficient avail ${CUR}:${av_now} (need ${pay_n})"
|
||||
SHOP_SKIP_N=$((SHOP_SKIP_N + 1))
|
||||
SHOP_REPORT="${SHOP_REPORT}${SHOP_REPORT:+ }${pname}=SKIP(bal)"
|
||||
continue
|
||||
|
|
@ -1286,9 +1494,9 @@ $SHOP_PICKED
|
|||
EOF
|
||||
INST="$_E2E_INST_SAVE"
|
||||
unset _E2E_INST_SAVE
|
||||
info "goa-shop summary" "$SHOP_REPORT (ok=$SHOP_OK_N fail=$SHOP_FAIL_N skip=$SHOP_SKIP_N · pick $PICK_N/$SHOP_CAT_N)"
|
||||
info "shop summary" "$SHOP_REPORT (ok=$SHOP_OK_N fail=$SHOP_FAIL_N skip=$SHOP_SKIP_N · pick $PICK_N/$SHOP_CAT_N)"
|
||||
if [ "$SHOP_OK" != "1" ] && [ "$SHOP_FAIL_N" -gt 0 ]; then
|
||||
warn "goa-shop" "no sample product payment succeeded ($SHOP_REPORT)"
|
||||
warn "shop" "no sample product payment succeeded ($SHOP_REPORT)"
|
||||
fi
|
||||
section "e2e · load snapshot (after shop pays)"
|
||||
metrics_report_load "${METRICS_DIR}/load-after-shop.json" "after-shop" || true
|
||||
|
|
@ -1297,6 +1505,7 @@ EOF
|
|||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
set_group paivana
|
||||
section "e2e · paivana paywall (template GOA:4200)"
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public paywall: https://paivana.hacktivism.ch → 302 to merchant template "paivana".
|
||||
|
|
@ -1388,11 +1597,12 @@ else
|
|||
info "paivana summary" "${PAIVANA_REPORT:-?} · instance ${E2E_PAIVANA_INSTANCE}"
|
||||
fi
|
||||
|
||||
set_group report
|
||||
section "e2e · report"
|
||||
info "user" "$USER"
|
||||
info "ATM withdraws" "$WITHDRAW_REPORT"
|
||||
info "payments" "$PAY_REPORT"
|
||||
info "goa-shop" "${SHOP_REPORT:-(n/a)}"
|
||||
info "shop" "${SHOP_REPORT:-(n/a)}"
|
||||
info "paivana" "${PAIVANA_REPORT:-(n/a)}"
|
||||
info "final balance" "$(fmt_bal "$SCRATCH/bal-live.out" 2>/dev/null || echo "(n/a)")"
|
||||
info "scratch" "$SCRATCH"
|
||||
|
|
|
|||
|
|
@ -29,7 +29,10 @@ ROOT=$(cd "$(dirname "$0")" && pwd)
|
|||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
# Area ladder.* — GOA withdraw/pay ladder
|
||||
# Groups: ladder.plan / ladder.load / ladder.withdraw / ladder.pay / ladder.report
|
||||
set_area ladder
|
||||
set_group plan
|
||||
SECTION_T0=$(date +%s)
|
||||
now_ms() { python3 -c 'import time; print(int(time.time()*1000))'; }
|
||||
elapsed_ms() {
|
||||
|
|
@ -261,6 +264,7 @@ info "steps" "${LADDER_STEPS} (0 + random≥${LADDER_MIN_AMOUNT} + max-1=${CUR}:
|
|||
info "withdraw_scale" "${LADDER_WITHDRAW_SCALE}× pay mids (fund pay ladder)"
|
||||
info "pay_phase" "$([ "${LADDER_PAY}" = "1" ] && echo enabled || echo disabled)"
|
||||
|
||||
set_group load
|
||||
section "ladder · load snapshot (before withdraws)"
|
||||
metrics_report_load "$LOAD_BEFORE" "ladder-start" || true
|
||||
# Fresh wallet per rung — baseline empty (or last-rung DB if re-used later)
|
||||
|
|
@ -344,6 +348,7 @@ STOP_REASON=""
|
|||
STOP_AMOUNT=""
|
||||
declare -a RUNG_JSON=()
|
||||
|
||||
set_group withdraw
|
||||
section "ladder · phase A · withdraw (${LADDER_N} rungs)"
|
||||
rung=0
|
||||
for AMT in "$@"; do
|
||||
|
|
@ -658,12 +663,7 @@ else:
|
|||
fi
|
||||
}
|
||||
|
||||
# Short bounded select assist (≤8s) — not a hang path; helps attach fresh reserve_pub
|
||||
if command -v perl >/dev/null 2>&1 && [ -f "$CLI_JS" ]; then
|
||||
perl -e 'alarm shift; exec @ARGV' 8 \
|
||||
node "$CLI_JS" --wallet-db="$WDB" --no-throttle run-until-done \
|
||||
>"$SCRATCH/select-$tag.out" 2>&1 || true
|
||||
fi
|
||||
# No run-until-done (hangs / banned). Read transactions only for reserve_pub candidates.
|
||||
wcli transactions >"$SCRATCH/tx-$tag.json" 2>&1 || true
|
||||
|
||||
t0=$(now_ms)
|
||||
|
|
@ -787,6 +787,7 @@ done
|
|||
PAY_OK_N=0
|
||||
PAY_FAIL_N=0
|
||||
if [ "${LADDER_PAY}" = "1" ] && [ -n "${PAY_LIST:-}" ] && [ "$FAIL_N_L" -eq 0 ]; then
|
||||
set_group pay
|
||||
section "ladder · phase B · pay"
|
||||
metrics_report_coins "before-pay-ladder" || true
|
||||
# Merchant secret (same as e2e)
|
||||
|
|
@ -959,19 +960,12 @@ except Exception: print("")
|
|||
ms_handle=$(elapsed_ms "$t0")
|
||||
ok "handle-uri $PAMT ${ms_handle}ms"
|
||||
|
||||
# Short settle polls (bounded — no infinite hang)
|
||||
# Short settle polls: merchant order + wallet tx only — never run-until-done
|
||||
t0=$(now_ms)
|
||||
settled=0
|
||||
r=0
|
||||
while [ "$r" -lt "${LADDER_PAY_SETTLE_ROUNDS}" ]; do
|
||||
r=$((r + 1))
|
||||
if command -v perl >/dev/null 2>&1; then
|
||||
perl -e 'alarm shift; exec @ARGV' 10 \
|
||||
"$CLI_JS" --wallet-db="$WDB" --no-throttle run-until-done \
|
||||
>"$SCRATCH/pay-run-$ptag.out" 2>&1 || true
|
||||
else
|
||||
wcli run-until-done >"$SCRATCH/pay-run-$ptag.out" 2>&1 || true
|
||||
fi
|
||||
wcli transactions >"$SCRATCH/tx-$ptag.out" 2>&1 || true
|
||||
curl -skS -m 8 -o "$SCRATCH/ord-paid-$ptag.json" -H "$AUTH" \
|
||||
"${MER}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
|
||||
|
|
@ -983,6 +977,7 @@ sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="p
|
|||
settled=1
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
ms_psettle=$(elapsed_ms "$t0")
|
||||
ms_total=$(elapsed_ms "$t_pay")
|
||||
|
|
@ -1024,6 +1019,7 @@ fi
|
|||
ms_phase=$(python3 -c 'import sys,time; print(int((time.time()-float(sys.argv[1]))*1000))' "$SECTION_T0")
|
||||
|
||||
# --- report ---
|
||||
set_group report
|
||||
section "ladder · report"
|
||||
info "auto-account" "$ACCT_USER"
|
||||
info "ok_rungs" "$OK_N"
|
||||
|
|
@ -1128,6 +1124,7 @@ PY
|
|||
|
||||
wcli balance 2>&1 | tee "$REPORT_DIR/balance-final.out" | tail -20 || true
|
||||
|
||||
set_group load
|
||||
section "ladder · load snapshot (after withdraws)"
|
||||
metrics_report_load "$LOAD_AFTER" "ladder-end" || true
|
||||
if [ -f "$LOAD_BEFORE" ] && [ -f "$LOAD_AFTER" ]; then
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ source "$ROOT/lib.sh"
|
|||
# shellcheck source=metrics.sh
|
||||
source "$ROOT/metrics.sh"
|
||||
|
||||
# Area inside-### — container / process state on koopa (SSH)
|
||||
# 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
|
||||
section "inside · collect from koopa"
|
||||
|
||||
if [ "${SKIP_SSH}" = "1" ]; then
|
||||
|
|
@ -128,10 +130,20 @@ if [ -z "$RAW" ] || ! echo "$RAW" | grep -q '^E|'; then
|
|||
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" ;;
|
||||
|
|
@ -143,6 +155,7 @@ while IFS= read -r line; do
|
|||
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
|
||||
|
|
|
|||
|
|
@ -23,10 +23,12 @@ json_currency() {
|
|||
python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d.get("currency") or "")' "$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Area sanity-### — public + optional server-side per component
|
||||
# Area sanity.* — public + optional server-side per component
|
||||
# Groups: sanity.bank / sanity.exchange / sanity.merchant
|
||||
set_area sanity
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
set_group bank
|
||||
section "sanity · bank"
|
||||
# ---------------------------------------------------------------------------
|
||||
expect_code "bank public /config" 200 "$BANK_PUBLIC/config"
|
||||
|
|
@ -98,6 +100,7 @@ else
|
|||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
set_group exchange
|
||||
section "sanity · exchange"
|
||||
# ---------------------------------------------------------------------------
|
||||
expect_code "exchange public /config" 200 "$EXCHANGE_PUBLIC/config"
|
||||
|
|
@ -193,6 +196,7 @@ else
|
|||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
set_group merchant
|
||||
section "sanity · merchant"
|
||||
# ---------------------------------------------------------------------------
|
||||
expect_code "merchant public /config" 200 "$MERCHANT_PUBLIC/config"
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ ROOT=$(cd "$(dirname "$0")" && pwd)
|
|||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
# Area server-### — host/container ports via SSH
|
||||
# Area server.* — host/container ports via SSH
|
||||
set_area server
|
||||
set_group ssh
|
||||
section "server · ssh ${KOOPA_SSH}"
|
||||
|
||||
if ! koopa_ssh_ok; then
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ ROOT=$(cd "$(dirname "$0")" && pwd)
|
|||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
# Area www-### — public HTTPS (outside-in)
|
||||
# Area www.* — public HTTPS (outside-in)
|
||||
# Groups: www.exchange / www.perf / www.stats / www.bank / www.merchant /
|
||||
# www.paivana / www.landing
|
||||
set_area www
|
||||
section "www · public URLs · ${TALER_DOMAIN:-?} (outside-in, no SSH)"
|
||||
|
||||
|
|
@ -169,7 +171,8 @@ sys.exit(0)
|
|||
PY
|
||||
}
|
||||
|
||||
# --- exchange (core; always required) --- www-001 …
|
||||
# --- exchange (core; always required) --- www.exchange-NN
|
||||
set_group exchange
|
||||
check_url "exchange /config" 200 "$EXCHANGE_PUBLIC/config"
|
||||
code=$(http_body "$EXCHANGE_PUBLIC/config" "$tmp/ec.json")
|
||||
if [ "$code" = "200" ]; then
|
||||
|
|
@ -219,6 +222,7 @@ fi
|
|||
# Performance — outside-in public HTTPS latency (this runner, not loopback)
|
||||
# Same spirit as bank landing-stats public probes; measured here from outside.
|
||||
# ---------------------------------------------------------------------------
|
||||
set_group perf
|
||||
section "www · performance · public HTTPS latency (outside-in)"
|
||||
|
||||
# Latency thresholds (ms), outside-in from this runner.
|
||||
|
|
@ -290,11 +294,18 @@ fi
|
|||
info "perf note" "RTT measured from this host (outside-in)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Landing load stats (stats.json written *inside* containers, usually public).
|
||||
# Prefer HTTPS /intro/stats.json (no SSH). If missing and SSH works, cat from
|
||||
# the container. Report loadavg + RSS + in-container probe ms — one line each.
|
||||
# Landing stats.json (public outside-in) — freshness + display fields.
|
||||
# Timestamps (required for external measurement):
|
||||
# generated_at_unix — epoch seconds (preferred)
|
||||
# generated_at — ISO-8601
|
||||
# generated_at_human — footer / “updated” UI string
|
||||
# Bank collector always sets all three; exchange/merchant scripts set unix too.
|
||||
# Env:
|
||||
# STATS_STALE_SECS warn if age > this (default 900 = 15m; timer often 5–15m)
|
||||
# STATS_FAIL_SECS hard fail if age > this (default 3600); 0 = never hard-fail on age
|
||||
# ---------------------------------------------------------------------------
|
||||
: "${STATS_STALE_SECS:=900}" # warn if generated_at older than 15m
|
||||
: "${STATS_STALE_SECS:=900}"
|
||||
: "${STATS_FAIL_SECS:=3600}"
|
||||
|
||||
_fetch_landing_stats_json() {
|
||||
# $1=name bank|exchange|merchant $2=base URL → writes $tmp/stats-$1.json, exit 0 if ok
|
||||
|
|
@ -322,81 +333,265 @@ _fetch_landing_stats_json() {
|
|||
return 1
|
||||
}
|
||||
|
||||
report_landing_load_stats() {
|
||||
# Validate one stats.json for monitoring display + outside age measurement.
|
||||
# Prints lines: OK|WARN|ERR|STALE|FAIL <detail>
|
||||
# Sets $tmp/stats-meta-$name.txt with gen_unix|source for shared-feed compare.
|
||||
check_landing_stats_json() {
|
||||
local name="$1" base="$2"
|
||||
local f="$tmp/stats-${name}.json"
|
||||
local line age_s
|
||||
local line hard_missing=0
|
||||
|
||||
if ! _fetch_landing_stats_json "$name" "$base"; then
|
||||
if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
|
||||
warn "perf ${name} landing-stats" "stats.json not available (public or SSH)"
|
||||
fail "stats ${name}" "HTTP/body missing · ${base}/intro/stats.json"
|
||||
else
|
||||
warn "stats ${name}" "stats.json not available"
|
||||
fi
|
||||
return
|
||||
return 1
|
||||
fi
|
||||
ok "stats ${name} reachable" "${base}/intro/stats.json"
|
||||
|
||||
# Multi-line verdict from python (STATUS detail)
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
META\ *)
|
||||
printf '%s\n' "${line#META }" >"$tmp/stats-meta-${name}.txt"
|
||||
;;
|
||||
OK\ *)
|
||||
ok "stats ${name}" "${line#OK }"
|
||||
;;
|
||||
WARN\ *)
|
||||
warn "stats ${name}" "${line#WARN }"
|
||||
;;
|
||||
STALE\ *)
|
||||
warn "stats ${name} freshness" "stale · ${line#STALE }"
|
||||
;;
|
||||
FAIL\ *)
|
||||
fail "stats ${name}" "${line#FAIL }"
|
||||
hard_missing=1
|
||||
;;
|
||||
ERR\ *)
|
||||
if [ "${LOCAL_STACK:-0}" = "1" ] || [ "${CHECK_LANDING:-1}" = "1" ]; then
|
||||
fail "stats ${name}" "${line#ERR }"
|
||||
else
|
||||
warn "stats ${name}" "${line#ERR }"
|
||||
fi
|
||||
hard_missing=1
|
||||
;;
|
||||
*)
|
||||
[ -n "$line" ] && warn "stats ${name}" "unparsed · $line"
|
||||
;;
|
||||
esac
|
||||
done < <(python3 - "$f" "$name" "${STATS_STALE_SECS}" "${STATS_FAIL_SECS}" <<'PY' 2>/dev/null || echo "ERR python check failed"
|
||||
import json, sys, time, re
|
||||
from datetime import datetime, timezone
|
||||
|
||||
path, role, stale_s, fail_s = sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4])
|
||||
now = int(time.time())
|
||||
|
||||
def out(kind, msg):
|
||||
print("%s %s" % (kind, msg))
|
||||
|
||||
line=$(python3 - "$f" "${STATS_STALE_SECS}" <<'PY' 2>/dev/null || true
|
||||
import json, sys, time
|
||||
path, stale = sys.argv[1], int(sys.argv[2])
|
||||
try:
|
||||
d = json.load(open(path))
|
||||
except Exception as e:
|
||||
print("ERR parse:" + str(e)[:80])
|
||||
out("ERR", "JSON parse: %s" % str(e)[:100])
|
||||
sys.exit(0)
|
||||
if not d.get("ok", True):
|
||||
print("ERR ok=false " + str(d.get("error") or "")[:80])
|
||||
|
||||
if not isinstance(d, dict):
|
||||
out("ERR", "body is not a JSON object")
|
||||
sys.exit(0)
|
||||
p = d.get("performance") or {}
|
||||
if not isinstance(p, dict) or not p:
|
||||
print("ERR no performance block")
|
||||
|
||||
if d.get("ok") is False:
|
||||
out("ERR", "ok=false · %s" % str(d.get("error") or d.get("hint") or "")[:80])
|
||||
sys.exit(0)
|
||||
mem = p.get("memory") if isinstance(p.get("memory"), dict) else {}
|
||||
rss = mem.get("container_rss_human") or mem.get("proc_sum_rss_human") or "?"
|
||||
load = p.get("loadavg") or "?"
|
||||
# latency fields differ by site
|
||||
bits = []
|
||||
for k in ("config_ms", "integration_ms", "webui_ms", "keys_ms", "terms_ms"):
|
||||
|
||||
# --- timestamps (outside-in age measurement) ---
|
||||
gu = d.get("generated_at_unix")
|
||||
giso = d.get("generated_at") or d.get("generated_at_iso")
|
||||
ghum = d.get("generated_at_human")
|
||||
age = None
|
||||
src_u = None
|
||||
|
||||
if gu is not None and str(gu).strip() != "":
|
||||
try:
|
||||
src_u = int(float(gu))
|
||||
# tolerate ms accidental timestamps
|
||||
if src_u > 10_000_000_000:
|
||||
src_u //= 1000
|
||||
age = now - src_u
|
||||
except Exception:
|
||||
out("ERR", "generated_at_unix not numeric: %r" % gu)
|
||||
src_u = None
|
||||
elif giso:
|
||||
# parse ISO when unix missing (legacy exchange/merchant before unix field)
|
||||
try:
|
||||
s = str(giso).strip()
|
||||
if s.endswith("Z"):
|
||||
s = s[:-1] + "+00:00"
|
||||
# allow +0200 without colon
|
||||
s = re.sub(r"([+-]\d{2})(\d{2})$", r"\1:\2", s)
|
||||
dt = datetime.fromisoformat(s)
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
src_u = int(dt.timestamp())
|
||||
age = now - src_u
|
||||
out("WARN", "no generated_at_unix — derived age from generated_at ISO (deploy collector with unix)")
|
||||
except Exception as e:
|
||||
out("ERR", "cannot parse generated_at=%r (%s)" % (giso, e))
|
||||
else:
|
||||
out("ERR", "missing generated_at_unix and generated_at — cannot measure freshness outside")
|
||||
|
||||
if not ghum and not giso:
|
||||
out("ERR", "missing generated_at_human/generated_at — UI cannot show “updated” time")
|
||||
elif ghum:
|
||||
out("OK", "timestamp human=%s" % ghum)
|
||||
else:
|
||||
out("OK", "timestamp iso=%s" % giso)
|
||||
|
||||
if src_u is not None:
|
||||
out("META", "%s|%s|%s" % (src_u, d.get("source") or "", d.get("currency") or ""))
|
||||
if age is not None and age < -120:
|
||||
out("WARN", "clock skew · generated_at_unix in the future by %ss" % (-age))
|
||||
if age is not None:
|
||||
out("OK", "generated_at_unix=%s age=%ss (now=%s)" % (src_u, age, now))
|
||||
if fail_s > 0 and age > fail_s:
|
||||
out("FAIL", "age=%ss > STATS_FAIL_SECS=%s · collector not updating" % (age, fail_s))
|
||||
elif age > stale_s:
|
||||
out("STALE", "age=%ss > STATS_STALE_SECS=%s · timer lag or stuck publish" % (age, stale_s))
|
||||
|
||||
# --- display fields for landings / monitoring ---
|
||||
src = str(d.get("source") or "")
|
||||
# Bank-shaped (incl. stage shared feed on all three landings)
|
||||
bankish = (
|
||||
role == "bank"
|
||||
or "collect_bank_stats" in src
|
||||
or "shared" in src
|
||||
or isinstance(d.get("withdraws"), dict)
|
||||
or isinstance(d.get("bank_accounts"), dict)
|
||||
)
|
||||
if bankish:
|
||||
missing = []
|
||||
if not isinstance(d.get("withdraws"), dict):
|
||||
missing.append("withdraws")
|
||||
ba = d.get("bank_accounts")
|
||||
wl = d.get("wallets")
|
||||
if not isinstance(ba, dict) and not isinstance(wl, dict):
|
||||
missing.append("bank_accounts|wallets")
|
||||
if d.get("currency") in (None, ""):
|
||||
missing.append("currency")
|
||||
if missing:
|
||||
out("ERR", "bank-shaped display missing: %s" % ",".join(missing))
|
||||
else:
|
||||
w = d.get("withdraws") or {}
|
||||
ba = ba if isinstance(ba, dict) else {}
|
||||
n_acc = ba.get("total") if ba.get("total") is not None else ba.get("users")
|
||||
out(
|
||||
"OK",
|
||||
"display bank-shape currency=%s accounts=%s withdraws.count=%s source=%s"
|
||||
% (d.get("currency"), n_acc, w.get("count"), (src or "?")[:48]),
|
||||
)
|
||||
elif src == "exchange-db" or (role == "exchange" and "reserves" in d):
|
||||
need = []
|
||||
for k in ("reserves", "wire_in_count", "known_coins"):
|
||||
if k not in d and k.replace("known_coins", "coins_live") not in d:
|
||||
if k == "known_coins" and ("coins_live" in d or "coins_remaining_amount" in d):
|
||||
continue
|
||||
need.append(k)
|
||||
if need:
|
||||
out("WARN", "exchange display keys missing: %s" % ",".join(need))
|
||||
else:
|
||||
out(
|
||||
"OK",
|
||||
"display exchange-db reserves=%s wire_in=%s coins=%s"
|
||||
% (d.get("reserves"), d.get("wire_in_count"), d.get("known_coins") or d.get("coins_live")),
|
||||
)
|
||||
elif src == "merchant-db" or (role == "merchant" and "instances" in d):
|
||||
if "instances" not in d and "orders" not in d:
|
||||
out("ERR", "merchant display missing instances/orders")
|
||||
else:
|
||||
out(
|
||||
"OK",
|
||||
"display merchant-db instances=%s orders=%s paid=%s"
|
||||
% (d.get("instances"), d.get("orders"), d.get("paid")),
|
||||
)
|
||||
else:
|
||||
out("WARN", "unknown stats schema role=%s source=%s keys=%s" % (role, src[:40], ",".join(list(d.keys())[:8])))
|
||||
|
||||
# performance block optional (stage shared bank feed may omit heavy probes)
|
||||
p = d.get("performance")
|
||||
if isinstance(p, dict) and p:
|
||||
mem = p.get("memory") if isinstance(p.get("memory"), dict) else {}
|
||||
rss = mem.get("container_rss_human") or mem.get("proc_sum_rss_human") or "—"
|
||||
load = p.get("loadavg") or "—"
|
||||
bits = []
|
||||
for k in ("config_ms", "integration_ms", "webui_ms", "keys_ms", "terms_ms"):
|
||||
if k in p and p[k] is not None:
|
||||
bits.append("%s=%sms" % (k.replace("_ms", ""), p[k]))
|
||||
lat = " ".join(bits) if bits else "latency=?"
|
||||
gen = d.get("generated_at_human") or d.get("generated_at") or "?"
|
||||
# staleness
|
||||
age = ""
|
||||
try:
|
||||
gu = d.get("generated_at_unix")
|
||||
if gu is not None:
|
||||
age_s = int(time.time()) - int(gu)
|
||||
age = " age=%ss" % age_s
|
||||
if age_s > stale:
|
||||
print("STALE loadavg=%s RSS=%s %s gen=%s%s" % (load, rss, lat, gen, age))
|
||||
sys.exit(0)
|
||||
except Exception:
|
||||
pass
|
||||
print("OK loadavg=%s RSS=%s %s gen=%s%s" % (load, rss, lat, gen, age))
|
||||
bits.append("%s=%s" % (k.replace("_ms", ""), p[k]))
|
||||
lat = " ".join(bits) if bits else "latency=—"
|
||||
out("OK", "performance loadavg=%s RSS=%s %s" % (load, rss, lat))
|
||||
else:
|
||||
out("OK", "performance block optional/absent")
|
||||
PY
|
||||
)
|
||||
|
||||
case "$line" in
|
||||
OK\ *)
|
||||
ok "perf ${name} landing-stats" "${line#OK }"
|
||||
;;
|
||||
STALE\ *)
|
||||
warn "perf ${name} landing-stats" "stale · ${line#STALE }"
|
||||
;;
|
||||
ERR\ *)
|
||||
warn "perf ${name} landing-stats" "${line#ERR }"
|
||||
;;
|
||||
*)
|
||||
warn "perf ${name} landing-stats" "unreadable stats.json"
|
||||
;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
|
||||
section "www · performance · landing load stats (stats.json · in-container probes)"
|
||||
report_landing_load_stats "bank" "$BANK_PUBLIC"
|
||||
report_landing_load_stats "exchange" "$EXCHANGE_PUBLIC"
|
||||
report_landing_load_stats "merchant" "$MERCHANT_PUBLIC"
|
||||
info "perf landing-stats note" "from /intro/stats.json (public); SSH container fallback if needed"
|
||||
set_group stats
|
||||
section "www · landing stats.json (freshness + display fields · outside-in)"
|
||||
info "stats policy" "STALE≥${STATS_STALE_SECS}s WARN · FAIL≥${STATS_FAIL_SECS}s ERROR (0=disable fail) · need generated_at_unix"
|
||||
check_landing_stats_json "bank" "$BANK_PUBLIC" || true
|
||||
check_landing_stats_json "exchange" "$EXCHANGE_PUBLIC" || true
|
||||
check_landing_stats_json "merchant" "$MERCHANT_PUBLIC" || true
|
||||
|
||||
# Shared feed: stage-style publish copies same bank stats to all three URLs
|
||||
if [ -f "$tmp/stats-meta-bank.txt" ] && [ -f "$tmp/stats-meta-exchange.txt" ] && [ -f "$tmp/stats-meta-merchant.txt" ]; then
|
||||
shared_line=$(python3 - "$tmp/stats-meta-bank.txt" "$tmp/stats-meta-exchange.txt" "$tmp/stats-meta-merchant.txt" <<'PY' 2>/dev/null || true
|
||||
import sys
|
||||
def parse(p):
|
||||
try:
|
||||
t = open(p).read().strip().split("|", 2)
|
||||
return t[0], (t[1] if len(t) > 1 else ""), (t[2] if len(t) > 2 else "")
|
||||
except Exception:
|
||||
return "", "", ""
|
||||
b, bs, bc = parse(sys.argv[1])
|
||||
e, es, ec = parse(sys.argv[2])
|
||||
m, ms, mc = parse(sys.argv[3])
|
||||
if not b or not e or not m:
|
||||
print("SKIP incomplete meta")
|
||||
elif b == e == m:
|
||||
print("SHARED gen_unix=%s bank_src=%s" % (b, (bs or "?")[:50]))
|
||||
else:
|
||||
# Shared feed only when all three claim the same bank-collector source
|
||||
# (e.g. stage TESTPAYSAN "… shared"). GOA uses independent exchange-db /
|
||||
# merchant-db collectors — different gen_unix is expected.
|
||||
def bankish(s):
|
||||
s = (s or "").lower()
|
||||
return "shared" in s or "collect_bank_stats" in s
|
||||
if bankish(bs) and bankish(es) and bankish(ms):
|
||||
print("DRIFT bank=%s exchange=%s merchant=%s (expected equal for shared feed)" % (b, e, m))
|
||||
else:
|
||||
print("INDEPENDENT bank=%s exchange=%s merchant=%s" % (b, e, m))
|
||||
PY
|
||||
)
|
||||
case "$shared_line" in
|
||||
SHARED\ *)
|
||||
ok "stats shared feed" "${shared_line#SHARED }"
|
||||
;;
|
||||
DRIFT\ *)
|
||||
warn "stats shared feed" "timestamps differ · ${shared_line#DRIFT }"
|
||||
;;
|
||||
INDEPENDENT\ *)
|
||||
info "stats feeds" "independent collectors · ${shared_line#INDEPENDENT }"
|
||||
;;
|
||||
*)
|
||||
info "stats feeds" "${shared_line:-n/a}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
info "stats note" "public GET /intro/stats.json; measure age via generated_at_unix vs wall clock"
|
||||
fi
|
||||
# Rollup + optional JSON for metrics_print_overall
|
||||
if [ -s "$PERF_TSV" ]; then
|
||||
|
|
@ -470,7 +665,8 @@ fi
|
|||
info "perf note" "measured from this host (outside-in); thresholds PERF_WARN_MS=${PERF_WARN_MS} PERF_FAIL_MS=${PERF_FAIL_MS}"
|
||||
|
||||
|
||||
# Terms + privacy (legal docs)
|
||||
# Exchange terms + privacy (same www.exchange group — issues map to exchange)
|
||||
set_group exchange
|
||||
check_legal_doc "exchange /terms" "$EXCHANGE_PUBLIC/terms" "terms|GOA|exploration|FADP|revDSG|privacy"
|
||||
check_legal_doc "exchange /privacy" "$EXCHANGE_PUBLIC/privacy" "privacy|FADP|revDSG|data|GOA|exploration"
|
||||
# trailing slash: 200 or redirect to bare path
|
||||
|
|
@ -485,6 +681,7 @@ case "$code" in
|
|||
esac
|
||||
|
||||
# --- bank ---
|
||||
set_group bank
|
||||
if [ "${LOCAL_STACK:-1}" = "0" ]; then
|
||||
check_url_soft "bank /config" 200 "$BANK_PUBLIC/config"
|
||||
else
|
||||
|
|
@ -508,7 +705,7 @@ if [ "$code" = "200" ]; then
|
|||
check_url_soft "bank /webui/" 200 "$BANK_PUBLIC/webui/"
|
||||
check_url_soft "bank /intro/" 200 "$BANK_PUBLIC/intro/"
|
||||
check_url_soft "bank /" 302,301,200 "$BANK_PUBLIC/"
|
||||
# Auto-account: credentials + shared-pool taler://withdraw (like step 2)
|
||||
# Auto-account: GOA shared-pool mint (hacktivism). Not used on TESTPAYSAN stage.
|
||||
aa_code=$(http_body "$BANK_PUBLIC/intro/auto-account.json" "$tmp/aa.json")
|
||||
case "$aa_code" in
|
||||
200)
|
||||
|
|
@ -518,11 +715,22 @@ if [ "$code" = "200" ]; then
|
|||
fail "bank /intro/auto-account.json" "invalid withdraw/login ($(tr '\n' ' ' <"$tmp/aa-val" | sed 's/[[:space:]]*$//'))"
|
||||
fi
|
||||
;;
|
||||
405|501|404|502|503|000)
|
||||
404|405|501)
|
||||
if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
|
||||
fail "bank /intro/auto-account.json" "HTTP $aa_code (want 200; 405/501 = broken)"
|
||||
else
|
||||
info "bank /intro/auto-account.json" "HTTP $aa_code — skip (not a GOA auto-account stack)"
|
||||
fi
|
||||
;;
|
||||
502|503|000)
|
||||
fail "bank /intro/auto-account.json" "HTTP $aa_code want 200"
|
||||
;;
|
||||
*)
|
||||
if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
|
||||
fail "bank /intro/auto-account.json" "HTTP $aa_code want 200"
|
||||
else
|
||||
warn "bank /intro/auto-account.json" "HTTP $aa_code (optional off-GOA)"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
|
@ -545,6 +753,7 @@ else
|
|||
fi
|
||||
|
||||
# --- merchant ---
|
||||
set_group merchant
|
||||
if [ "${LOCAL_STACK:-1}" = "0" ]; then
|
||||
check_url_soft "merchant /config" 200 "$MERCHANT_PUBLIC/config"
|
||||
else
|
||||
|
|
@ -610,6 +819,7 @@ esac
|
|||
# Paivana paywall (local GOA stack) — public front only; pay path is e2e
|
||||
# ---------------------------------------------------------------------------
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ] && [ "${E2E_PAIVANA:-1}" != "0" ]; then
|
||||
set_group paivana
|
||||
section "www · paivana paywall"
|
||||
: "${PAIVANA_PUBLIC:=https://paivana.hacktivism.ch}"
|
||||
PAIVANA_PUBLIC="${PAIVANA_PUBLIC%/}"
|
||||
|
|
@ -646,6 +856,7 @@ if [ "${CHECK_LANDING:-1}" != "1" ]; then
|
|||
exit 0
|
||||
fi
|
||||
|
||||
set_group landing
|
||||
section "www · landing exposed links · bank / merchant / exchange"
|
||||
|
||||
# Probe one URL: print code to stdout (200 after following redirects counts as 200).
|
||||
|
|
@ -803,18 +1014,24 @@ check_one_landing() {
|
|||
return
|
||||
fi
|
||||
|
||||
# Required static assets (hard: qrcode + og; soft: qr-logo)
|
||||
for url in \
|
||||
"${base}/intro/qrcode.min.js" \
|
||||
"${base}/intro/og-goa-shop.png"
|
||||
do
|
||||
if _landing_probe "$url"; then
|
||||
# Required static: qrcode.min.js. GOA og image is soft off-GOA (stage has no og-goa-shop.png).
|
||||
if _landing_probe "${base}/intro/qrcode.min.js"; then
|
||||
a_ok=$((a_ok + 1))
|
||||
else
|
||||
a_fail=$((a_fail + 1))
|
||||
fail_sample="${fail_sample}${fail_sample:+; }HTTP ${_landing_code} $url"
|
||||
fail_sample="${fail_sample}${fail_sample:+; }HTTP ${_landing_code} ${base}/intro/qrcode.min.js"
|
||||
fi
|
||||
if _landing_probe "${base}/intro/og-goa-shop.png"; then
|
||||
a_ok=$((a_ok + 1))
|
||||
else
|
||||
if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
|
||||
a_fail=$((a_fail + 1))
|
||||
fail_sample="${fail_sample}${fail_sample:+; }HTTP ${_landing_code} ${base}/intro/og-goa-shop.png"
|
||||
else
|
||||
a_soft=$((a_soft + 1))
|
||||
soft_sample="${soft_sample}${soft_sample:+; }HTTP ${_landing_code} og-goa-shop.png (GOA asset)"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if _landing_probe "${base}/intro/qr-logo.png"; then
|
||||
a_ok=$((a_ok + 1))
|
||||
else
|
||||
|
|
@ -942,7 +1159,15 @@ if [ "${LOCAL_STACK:-1}" = "1" ] || [ -n "${BANK_PUBLIC:-}" ]; then
|
|||
fail "bank /intro/demo-withdraw.json" "invalid taler://withdraw ($(tr '\n' ' ' <"$tmp/dw-val" | sed 's/[[:space:]]*$//'))"
|
||||
fi
|
||||
;;
|
||||
405|501|404|502|503|000)
|
||||
404|405|501)
|
||||
# GOA bank landing mints demo-withdraw.json; stage TESTPAYSAN uses ATM/wallet guide only
|
||||
if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
|
||||
fail "landing bank demo-withdraw" "HTTP $dw_code (want 200)"
|
||||
else
|
||||
info "landing bank demo-withdraw" "HTTP $dw_code — skip (not a GOA demo-withdraw stack)"
|
||||
fi
|
||||
;;
|
||||
502|503|000)
|
||||
fail "landing bank demo-withdraw" "HTTP $dw_code (want 200)"
|
||||
;;
|
||||
*)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ ROOT=$(cd "$(dirname "$0")" && pwd)
|
|||
source "$ROOT/lib.sh"
|
||||
|
||||
set_area versions
|
||||
set_group outside
|
||||
|
||||
SUITE="${TALER_APT_SUITE:-trixie}"
|
||||
APT_BASE="${TALER_APT_BASE:-https://deb.taler.net/apt/debian}"
|
||||
|
|
@ -166,6 +167,7 @@ fi
|
|||
# ---------------------------------------------------------------------------
|
||||
# 2) INSIDE — containers can reach deb.taler.net (pasta / install path)
|
||||
# ---------------------------------------------------------------------------
|
||||
set_group inside
|
||||
section "versions · inside · containers → deb.taler.net"
|
||||
|
||||
if [ "${SKIP_SSH}" = "1" ]; then
|
||||
|
|
@ -266,6 +268,7 @@ ok "collected installed packages" "${n_inst} rows"
|
|||
# ---------------------------------------------------------------------------
|
||||
# 3) compare installed vs trixie (and note testing)
|
||||
# ---------------------------------------------------------------------------
|
||||
set_group compare
|
||||
section "versions · compare installed vs ${SUITE}"
|
||||
|
||||
export BEHIND_MODE HAVE_TESTING
|
||||
|
|
|
|||
|
|
@ -193,6 +193,13 @@ apply_taler_domain() {
|
|||
PAY_AMT="${PAY_AMT:-${EXPECT_CURRENCY}:0.01}"
|
||||
CREDIT_AMT="${CREDIT_AMT:-${EXPECT_CURRENCY}:100}"
|
||||
;;
|
||||
TESTPAYSAN)
|
||||
# Stage FrancPaysan: default instance + public templates (e2e-001/005/1)
|
||||
MERCHANT_INSTANCE="${MERCHANT_INSTANCE:-default}"
|
||||
WITHDRAW_AMT="${WITHDRAW_AMT:-TESTPAYSAN:20}"
|
||||
PAY_AMT="${PAY_AMT:-TESTPAYSAN:0.01}"
|
||||
CREDIT_AMT="${CREDIT_AMT:-TESTPAYSAN:100}"
|
||||
;;
|
||||
CHF)
|
||||
WITHDRAW_AMT="${WITHDRAW_AMT:-CHF:20}"
|
||||
PAY_AMT="${PAY_AMT:-CHF:0.01}"
|
||||
|
|
@ -352,17 +359,30 @@ koopa_ssh_python() {
|
|||
with_timeout "$t" ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" python3 -
|
||||
}
|
||||
|
||||
# ANSI colours for tags (green OK / yellow WARN / red ERROR·BLOCKER / cyan INFO).
|
||||
# Default: always on so pasted logs keep visible tags. Disable: NO_COLOR=1 or CLICOLOR=0.
|
||||
# ANSI colours — boxed badges (fg+bg) + body (label bold, detail dim).
|
||||
# Off: NO_COLOR=1 or CLICOLOR=0.
|
||||
if [ "${NO_COLOR:-0}" = "1" ] || [ "${CLICOLOR:-1}" = "0" ]; then
|
||||
G= R= Y= C= N= B=
|
||||
G= R= Y= C= M= D= W= B= N=
|
||||
BG_OK= BG_INFO= BG_WARN= BG_ERR= BG_BLK= BG_SEC=
|
||||
BOX=0
|
||||
else
|
||||
G=$'\e[1;32m' # bold green [OK]
|
||||
R=$'\e[1;31m' # bold red [ERROR] [BLOCKER]
|
||||
Y=$'\e[1;33m' # bold yellow [WARN]
|
||||
C=$'\e[1;36m' # bold cyan [INFO]
|
||||
B=$'\e[1m'
|
||||
N=$'\e[0m'
|
||||
G=$'\e[1;32m' # green text (totals)
|
||||
R=$'\e[1;31m' # red text
|
||||
Y=$'\e[1;33m' # yellow text
|
||||
C=$'\e[1;36m' # cyan text
|
||||
M=$'\e[1;35m' # magenta text
|
||||
D=$'\e[2m' # dim tid / detail
|
||||
W=$'\e[1;37m' # bold white label
|
||||
B=$'\e[1m' # bold section
|
||||
N=$'\e[0m' # reset
|
||||
# Badge fill: bright text on solid background
|
||||
BG_OK=$'\e[1;30;42m' # black on green
|
||||
BG_INFO=$'\e[1;30;46m' # black on cyan
|
||||
BG_WARN=$'\e[1;30;43m' # black on yellow
|
||||
BG_ERR=$'\e[1;37;41m' # white on red
|
||||
BG_BLK=$'\e[1;37;45m' # white on magenta
|
||||
BG_SEC=$'\e[1;37;44m' # white on blue (section)
|
||||
BOX=1
|
||||
fi
|
||||
|
||||
PASS_N=0
|
||||
|
|
@ -372,17 +392,68 @@ INFO_N=0
|
|||
BLOCKERS=() # human-readable payment/withdraw blockers
|
||||
ERRORS=() # all ERROR lines (component scope)
|
||||
|
||||
# Test IDs by area: www-001, e2e-001, inside-001, …
|
||||
# Usage: set_area www then each ok/fail/warn/info/blocker/err auto-numbers.
|
||||
# Grouped test IDs: area.group-NN (e.g. www.exchange-01, e2e.pay-03)
|
||||
# set_area www # phase: www | e2e | inside | versions | …
|
||||
# set_group exchange # resets counter + prints group box → www.exchange-01
|
||||
# set_group bank # → www.bank-01
|
||||
# Without set_group: area-01, area-02, … (flat within area)
|
||||
TEST_AREA=""
|
||||
TEST_GROUP=""
|
||||
TEST_N=0
|
||||
# Last issued id (www-001); set by _take_tid — not via $(…) so TEST_N persists.
|
||||
LAST_TID=""
|
||||
# Per-group counters so re-entering www.exchange after perf continues NN
|
||||
# (shell vars TEST_CNT_<area>_<group>).
|
||||
_tid_key() {
|
||||
# $1=area $2=group → safe identifier
|
||||
printf 'TEST_CNT_%s_%s' "$1" "$2" | tr -c 'A-Za-z0-9_' '_'
|
||||
}
|
||||
_tid_save() {
|
||||
[ -z "${TEST_AREA:-}" ] || [ -z "${TEST_GROUP:-}" ] && return 0
|
||||
local k
|
||||
k=$(_tid_key "$TEST_AREA" "$TEST_GROUP")
|
||||
eval "$k=\$TEST_N"
|
||||
}
|
||||
_tid_load() {
|
||||
local k
|
||||
k=$(_tid_key "$TEST_AREA" "$1")
|
||||
eval "TEST_N=\${$k:-0}"
|
||||
}
|
||||
set_area() {
|
||||
# leave previous group counter saved
|
||||
_tid_save
|
||||
TEST_AREA="$1"
|
||||
TEST_GROUP=""
|
||||
TEST_N=0
|
||||
LAST_TID=""
|
||||
}
|
||||
# Start a logical sub-group. Short stable names:
|
||||
# exchange bank merchant perf stats landing paivana
|
||||
# prereq wallet atm settle pay shop dig load report
|
||||
# ssh caddy outside inside compare withdraw
|
||||
# Counter continues if the same group is re-entered later in the area.
|
||||
# Prints a compact group chip so log + issue text map cleanly (www.bank-03).
|
||||
set_group() {
|
||||
local g="$1"
|
||||
# no-op if same group already active (e.g. inside emit loop)
|
||||
if [ "$g" = "${TEST_GROUP:-}" ] && [ -n "$g" ]; then
|
||||
return 0
|
||||
fi
|
||||
_tid_save
|
||||
TEST_GROUP="$g"
|
||||
LAST_TID=""
|
||||
if [ -z "${TEST_AREA:-}" ] || [ -z "$g" ]; then
|
||||
TEST_N=0
|
||||
return 0
|
||||
fi
|
||||
_tid_load "$g"
|
||||
local tag="${TEST_AREA}.${g}"
|
||||
if [ "${BOX:-0}" = "1" ]; then
|
||||
# cyan-outline group chip: ┌ www.exchange ┐
|
||||
printf '%s%s┌ %s ┐%s\n' "$D" "$C" "$tag" "$N"
|
||||
else
|
||||
printf -- '-- %s --\n' "$tag"
|
||||
fi
|
||||
}
|
||||
# Assign next id into LAST_TID (must not run in a subshell).
|
||||
_take_tid() {
|
||||
LAST_TID=""
|
||||
|
|
@ -390,49 +461,69 @@ _take_tid() {
|
|||
return
|
||||
fi
|
||||
TEST_N=$((TEST_N + 1))
|
||||
LAST_TID=$(printf '%s-%03d' "$TEST_AREA" "$TEST_N")
|
||||
if [ -n "${TEST_GROUP:-}" ]; then
|
||||
LAST_TID=$(printf '%s.%s-%02d' "$TEST_AREA" "$TEST_GROUP" "$TEST_N")
|
||||
_tid_save
|
||||
else
|
||||
LAST_TID=$(printf '%s-%02d' "$TEST_AREA" "$TEST_N")
|
||||
fi
|
||||
}
|
||||
# Dim test id: "www.exchange-01 " or empty
|
||||
_fmt_tid() {
|
||||
# prefix "www-001 " or empty
|
||||
if [ -n "${LAST_TID:-}" ]; then
|
||||
printf '%s ' "$LAST_TID"
|
||||
printf '%s%s%s ' "$D" "$LAST_TID" "$N"
|
||||
fi
|
||||
}
|
||||
# Boxed badge cell: ┌ OK ┐ with filled bg (tag left-padded, width 7 for BLOCKER)
|
||||
# $1=badge style $2=tag text
|
||||
_fmt_badge() {
|
||||
local badge="$1" tag="$2" inner
|
||||
inner=$(printf -- '%-7s' "$tag")
|
||||
if [ "${BOX:-0}" = "1" ]; then
|
||||
printf '%s┌ %s┐%s' "$badge" "$inner" "$N"
|
||||
else
|
||||
printf '%s[ %s]%s' "$badge" "$inner" "$N"
|
||||
fi
|
||||
}
|
||||
# One concrete line: ┌ OK ┐ tid label · detail
|
||||
# $1=badge style $2=tag text $3=label colour $4=label $5=detail
|
||||
_msg_line() {
|
||||
local badge="$1" tag="$2" lcol="$3" label="$4" detail="${5:-}"
|
||||
if [ -n "$detail" ]; then
|
||||
printf -- '%s %s%s%s%s %s·%s %s%s%s\n' \
|
||||
"$(_fmt_badge "$badge" "$tag")" "$(_fmt_tid)" "$lcol" "$label" "$N" "$D" "$N" "$D" "$detail" "$N"
|
||||
else
|
||||
printf -- '%s %s%s%s%s\n' \
|
||||
"$(_fmt_badge "$badge" "$tag")" "$(_fmt_tid)" "$lcol" "$label" "$N"
|
||||
fi
|
||||
}
|
||||
|
||||
ok() {
|
||||
# Forms (same idea as info/warn):
|
||||
# ok "what passed"
|
||||
# ok "what passed" "detail / ms / bytes / …"
|
||||
# ok "what is good" ["concrete evidence: HTTP 200 · 12ms · …"]
|
||||
local label="$1" detail="${2:-}"
|
||||
_take_tid
|
||||
if [ -n "$detail" ]; then
|
||||
printf '%s[OK]%s %s%s — %s\n' "$G" "$N" "$(_fmt_tid)" "$label" "$detail"
|
||||
else
|
||||
printf '%s[OK]%s %s%s\n' "$G" "$N" "$(_fmt_tid)" "$label"
|
||||
fi
|
||||
_msg_line "$BG_OK" "OK" "$W" "$label" "$detail"
|
||||
PASS_N=$((PASS_N + 1))
|
||||
}
|
||||
# component-scoped error: err bank "libeufin down" "detail"
|
||||
# component-scoped error: err bank "what failed" "why / HTTP / path"
|
||||
err() {
|
||||
local comp="$1" msg="$2" detail="${3:-}"
|
||||
_take_tid
|
||||
printf '%s[ERROR]%s %s%s: %s%s\n' "$R" "$N" "$(_fmt_tid)" "$comp" "$msg" "${detail:+ — $detail}"
|
||||
_msg_line "$BG_ERR" "ERROR" "$W" "${comp}: ${msg}" "$detail"
|
||||
FAIL_N=$((FAIL_N + 1))
|
||||
ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ — $detail}")
|
||||
ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ · $detail}")
|
||||
}
|
||||
# legacy fail label ...
|
||||
# fail "what failed" ["why / HTTP code / path"]
|
||||
fail() {
|
||||
local label="$1" detail="${2:-}"
|
||||
_take_tid
|
||||
printf '%s[ERROR]%s %s%s%s\n' "$R" "$N" "$(_fmt_tid)" "$label" "${detail:+ — $detail}"
|
||||
_msg_line "$BG_ERR" "ERROR" "$W" "$label" "$detail"
|
||||
FAIL_N=$((FAIL_N + 1))
|
||||
ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ — $detail}")
|
||||
ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ · $detail}")
|
||||
}
|
||||
warn() {
|
||||
# Forms (same idea as err; always try to state the problem):
|
||||
# warn "problem"
|
||||
# warn "problem" "why / context"
|
||||
# warn component "problem" "why / context"
|
||||
# warn "what is soft-bad" ["why still ok to continue"]
|
||||
# warn component "what" "why"
|
||||
local a1="${1:-}" a2="${2:-}" a3="${3:-}"
|
||||
local head detail
|
||||
_take_tid
|
||||
|
|
@ -446,56 +537,78 @@ warn() {
|
|||
head="$a1"
|
||||
detail=""
|
||||
fi
|
||||
if [ -n "$detail" ]; then
|
||||
printf '%s[WARN]%s %s%s — %s\n' "$Y" "$N" "$(_fmt_tid)" "$head" "$detail"
|
||||
else
|
||||
printf '%s[WARN]%s %s%s\n' "$Y" "$N" "$(_fmt_tid)" "$head"
|
||||
fi
|
||||
_msg_line "$BG_WARN" "WARN" "$W" "$head" "$detail"
|
||||
WARN_N=$((WARN_N + 1))
|
||||
}
|
||||
info() {
|
||||
# info "topic" ["concrete fact / value / next step"]
|
||||
local label="$1" detail="${2:-}"
|
||||
_take_tid
|
||||
if [ -n "$detail" ]; then
|
||||
printf '%s[INFO]%s %s%s — %s\n' "$C" "$N" "$(_fmt_tid)" "$label" "$detail"
|
||||
else
|
||||
printf '%s[INFO]%s %s%s\n' "$C" "$N" "$(_fmt_tid)" "$label"
|
||||
fi
|
||||
_msg_line "$BG_INFO" "INFO" "$W" "$label" "$detail"
|
||||
INFO_N=$((INFO_N + 1))
|
||||
}
|
||||
blocker() {
|
||||
# Payment/withdraw path cannot proceed because of this
|
||||
# Hard stop on pay/withdraw path: blocker "step" "why it cannot continue"
|
||||
local step="$1" msg="$2"
|
||||
_take_tid
|
||||
printf '%s[BLOCKER]%s %s%s: %s\n' "$R$B" "$N" "$(_fmt_tid)" "$step" "$msg"
|
||||
_msg_line "$BG_BLK" "BLOCKER" "$W" "${step}" "$msg"
|
||||
BLOCKERS+=("${LAST_TID:+$LAST_TID }[$step] $msg")
|
||||
FAIL_N=$((FAIL_N + 1))
|
||||
ERRORS+=("BLOCKER ${LAST_TID:+$LAST_TID }[$step] $msg")
|
||||
}
|
||||
section() { printf '\n%s== %s ==%s\n' "$B" "$*" "$N"; }
|
||||
section() {
|
||||
# Boxed section header (3 lines when colour on)
|
||||
local title="$*"
|
||||
local w=${#title}
|
||||
[ "$w" -lt 24 ] && w=24
|
||||
[ "$w" -gt 56 ] && w=56
|
||||
local pad line i
|
||||
pad=$(printf -- "%-${w}s" "$title")
|
||||
if [ "${BOX:-0}" = "1" ]; then
|
||||
line=""
|
||||
i=0
|
||||
while [ "$i" -lt $((w + 2)) ]; do
|
||||
line="${line}═"
|
||||
i=$((i + 1))
|
||||
done
|
||||
printf -- '\n%s╔%s╗%s\n' "$BG_SEC" "$line" "$N"
|
||||
printf -- '%s║%s %s%s%s %s║%s\n' "$BG_SEC" "$N" "$B" "$pad" "$N" "$BG_SEC" "$N"
|
||||
printf -- '%s╚%s╝%s\n' "$BG_SEC" "$line" "$N"
|
||||
else
|
||||
printf -- '\n== %s ==\n' "$title"
|
||||
fi
|
||||
}
|
||||
|
||||
summary() {
|
||||
echo ""
|
||||
if [ "${#BLOCKERS[@]}" -gt 0 ]; then
|
||||
printf '%s--- BLOCKERS (fix/withdraw path) ---%s\n' "$R$B" "$N"
|
||||
if [ "${BOX:-0}" = "1" ]; then
|
||||
printf -- '%s┌ BLOCKERS · pay/withdraw cannot finish ┐%s\n' "$BG_BLK" "$N"
|
||||
else
|
||||
printf -- '--- BLOCKERS (pay/withdraw cannot finish) ---\n'
|
||||
fi
|
||||
local b
|
||||
for b in "${BLOCKERS[@]}"; do
|
||||
printf '%s • %s%s\n' "$R" "$b" "$N"
|
||||
printf -- '%s •%s %s%s%s\n' "$M" "$N" "$W" "$b" "$N"
|
||||
done
|
||||
fi
|
||||
if [ "${#ERRORS[@]}" -gt 0 ] && [ "${#BLOCKERS[@]}" -lt "${#ERRORS[@]}" ]; then
|
||||
printf '%s--- ERRORS ---%s\n' "$R" "$N"
|
||||
if [ "${BOX:-0}" = "1" ]; then
|
||||
printf -- '%s┌ ERRORS · failed checks ┐%s\n' "$BG_ERR" "$N"
|
||||
else
|
||||
printf -- '--- ERRORS (failed checks) ---\n'
|
||||
fi
|
||||
local e
|
||||
for e in "${ERRORS[@]}"; do
|
||||
case "$e" in BLOCKER*) continue ;; esac
|
||||
printf '%s • %s%s\n' "$R" "$e" "$N"
|
||||
printf -- '%s •%s %s%s%s\n' "$R" "$N" "$W" "$e" "$N"
|
||||
done
|
||||
fi
|
||||
printf 'totals: %s%d OK%s' "$G" "$PASS_N" "$N"
|
||||
[ "$FAIL_N" -gt 0 ] && printf ', %s%d ERROR%s' "$R" "$FAIL_N" "$N"
|
||||
[ "$WARN_N" -gt 0 ] && printf ', %s%d WARN%s' "$Y" "$WARN_N" "$N"
|
||||
[ "$INFO_N" -gt 0 ] && printf ', %d INFO' "$INFO_N"
|
||||
[ "${#BLOCKERS[@]}" -gt 0 ] && printf ', %s%d BLOCKER%s' "$R" "${#BLOCKERS[@]}" "$N"
|
||||
printf -- '%stotals:%s %s%d OK%s' "$D" "$N" "$G" "$PASS_N" "$N"
|
||||
[ "$FAIL_N" -gt 0 ] && printf -- ', %s%d ERROR%s' "$R" "$FAIL_N" "$N"
|
||||
[ "$WARN_N" -gt 0 ] && printf -- ', %s%d WARN%s' "$Y" "$WARN_N" "$N"
|
||||
[ "$INFO_N" -gt 0 ] && printf -- ', %s%d INFO%s' "$C" "$INFO_N" "$N"
|
||||
[ "${#BLOCKERS[@]}" -gt 0 ] && printf -- ', %s%d BLOCKER%s' "$M" "${#BLOCKERS[@]}" "$N"
|
||||
printf '\n'
|
||||
[ "$FAIL_N" -eq 0 ]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue