Compare commits
No commits in common. "e46aeb408af803cbf08ee295f6948f8ab647dc16" and "d6f4f649c1d151e4250a75999dc39320d163879f" have entirely different histories.
e46aeb408a
...
d6f4f649c1
19 changed files with 465 additions and 1204 deletions
|
|
@ -14,14 +14,12 @@ TimeoutStartSec=480
|
||||||
# Do not keep "active" after run — timer may fire again cleanly
|
# Do not keep "active" after run — timer may fire again cleanly
|
||||||
RemainAfterExit=no
|
RemainAfterExit=no
|
||||||
Environment=TZ=Europe/Zurich
|
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=ADMIN_LOG=%h/src/koopa/koopa-admin-log
|
||||||
Environment=SECRETS_BANK=%h/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank
|
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_URL=http://127.0.0.1:9012
|
||||||
Environment=BANK_PUBLIC_URL=https://bank.hacktivism.ch
|
Environment=BANK_PUBLIC_URL=https://bank.hacktivism.ch
|
||||||
Environment=EXCHANGE_CONFIG_URL=https://exchange.hacktivism.ch/config
|
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=SCAN_SKIP=exchange
|
||||||
Environment=TX_PAGE=500
|
Environment=TX_PAGE=500
|
||||||
Environment=MAX_TX_PAGES=0
|
Environment=MAX_TX_PAGES=0
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,6 @@ fi
|
||||||
|
|
||||||
GEN=$(now_iso)
|
GEN=$(now_iso)
|
||||||
HUMAN=$(now_human)
|
HUMAN=$(now_human)
|
||||||
GEN_UNIX=$(date +%s)
|
|
||||||
num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; }
|
num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; }
|
||||||
|
|
||||||
MEM_JSON='"container_rss_human": "—"'
|
MEM_JSON='"container_rss_human": "—"'
|
||||||
|
|
@ -258,7 +257,6 @@ cat >"$TMP" <<EOF
|
||||||
"timezone": $(json_str "$TZ"),
|
"timezone": $(json_str "$TZ"),
|
||||||
"generated_at": $(json_str "$GEN"),
|
"generated_at": $(json_str "$GEN"),
|
||||||
"generated_at_human": $(json_str "$HUMAN"),
|
"generated_at_human": $(json_str "$HUMAN"),
|
||||||
"generated_at_unix": ${GEN_UNIX},
|
|
||||||
"reserves": ${RESERVES:-0},
|
"reserves": ${RESERVES:-0},
|
||||||
"wire_in_count": ${RESERVES_IN:-0},
|
"wire_in_count": ${RESERVES_IN:-0},
|
||||||
"wire_in_amount": $(json_str "$WIRE_IN_AMT"),
|
"wire_in_amount": $(json_str "$WIRE_IN_AMT"),
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,12 @@
|
||||||
# taler-landing — generic multi-stack landing stats
|
# taler-landing — central stats (hernani user systemd)
|
||||||
|
|
||||||
One collector (`collect_bank_stats.py` + `collect-landing-stats.sh`) for any
|
Public landing numbers on
|
||||||
currency / stack. **Defaults = GOA / hacktivism**; stage TESTPAYSAN uses a
|
|
||||||
profile file.
|
|
||||||
|
|
||||||
Public landing numbers (example GOA):
|
|
||||||
|
|
||||||
- https://bank.hacktivism.ch/intro/ → `stats.json`
|
- https://bank.hacktivism.ch/intro/ → `stats.json`
|
||||||
- https://exchange.hacktivism.ch/intro/ → `stats.json`
|
- https://exchange.hacktivism.ch/intro/ → `stats.json`
|
||||||
- https://taler.hacktivism.ch/intro/ → `stats.json`
|
- https://taler.hacktivism.ch/intro/ → `stats.json`
|
||||||
|
|
||||||
Stage TESTPAYSAN (shared bank feed on all three hosts):
|
are produced by **one host process** as user **`hernani`**, not by three unrelated in-container crons.
|
||||||
|
|
||||||
- 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
|
## Why host / user systemd
|
||||||
|
|
||||||
|
|
@ -86,37 +77,16 @@ Preferred order:
|
||||||
|
|
||||||
Explorer password optional (shared-pool balance).
|
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 overrides
|
||||||
|
|
||||||
| Env | Default | Meaning |
|
| Env | Default | Meaning |
|
||||||
|-----|---------|---------|
|
|-----|---------|---------|
|
||||||
| `BANK_URL` | `http://127.0.0.1:9012` | libeufin loopback |
|
| `BANK_URL` | `http://127.0.0.1:9012` | libeufin loopback |
|
||||||
| `BANK_CURRENCY` | *(auto)* | `GOA` / `TESTPAYSAN` / … |
|
|
||||||
| `SCAN_SKIP` | `exchange` | usernames excluded from flow |
|
| `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 |
|
| `TX_PAGE` | `500` | transactions page size |
|
||||||
| `MAX_TX_PAGES` | `0` | `0` = unlimited pages / account |
|
| `MAX_TX_PAGES` | `0` | `0` = unlimited pages / account |
|
||||||
| `ACCOUNTS_DELTA` | `-10000` | account list window |
|
| `ACCOUNTS_DELTA` | `-10000` | account list window |
|
||||||
| `ADMIN_LOG` | `%h/src/koopa/koopa-admin-log` | refresh in-container scripts |
|
| `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
|
## UI alt names
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Generic landing-stats orchestrator (any Taler stack).
|
# Central landing-stats collector for hernani@koopa (user systemd timer).
|
||||||
#
|
#
|
||||||
# Defaults = GOA / hacktivism on koopa. Override via env or EnvironmentFile
|
# - Bank: full account+ledger scan via collect_bank_stats.py → bank container
|
||||||
# (see profiles: goa | stage-testpaysan).
|
# - Exchange / merchant: existing in-container scripts, then amount_alt enrich
|
||||||
#
|
# - All three: container RSS / loadavg / top procs via mem-snapshot (podman exec)
|
||||||
# - Bank: collect_bank_stats.py (currency-agnostic) → stats.json
|
# - Never wipes a good stats.json on failure (writes stats-run.json only)
|
||||||
# - 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
|
# Install: scripts/taler-landing/install-landing-stats-host.sh
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
@ -16,20 +12,6 @@ set -euo pipefail
|
||||||
export TZ="${TZ:-Europe/Zurich}"
|
export TZ="${TZ:-Europe/Zurich}"
|
||||||
export PATH="${HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin${PATH:+:$PATH}"
|
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)" "$*"; }
|
log() { printf '%s %s\n' "$(date -Iseconds)" "$*"; }
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
@ -40,8 +22,6 @@ elif [ -f "${HOME}/.local/lib/taler-landing/collect_bank_stats.py" ]; then
|
||||||
LIB="${HOME}/.local/lib/taler-landing"
|
LIB="${HOME}/.local/lib/taler-landing"
|
||||||
elif [ -f "${HOME}/src/koopa/koopa-admin-log/scripts/taler-landing/collect_bank_stats.py" ]; then
|
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"
|
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
|
else
|
||||||
LIB="$ROOT"
|
LIB="$ROOT"
|
||||||
fi
|
fi
|
||||||
|
|
@ -49,13 +29,6 @@ fi
|
||||||
ADMIN_LOG="${ADMIN_LOG:-${HOME}/src/koopa/koopa-admin-log}"
|
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}"
|
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}"
|
BANK_CTR="${BANK_CTR:-taler-hacktivism-bank}"
|
||||||
EX_CTR="${EX_CTR:-taler-hacktivism-exchange-ansible}"
|
EX_CTR="${EX_CTR:-taler-hacktivism-exchange-ansible}"
|
||||||
MER_CTR="${MER_CTR:-taler-hacktivism}"
|
MER_CTR="${MER_CTR:-taler-hacktivism}"
|
||||||
|
|
@ -63,19 +36,10 @@ MER_CTR="${MER_CTR:-taler-hacktivism}"
|
||||||
BANK_URL="${BANK_URL:-http://127.0.0.1:9012}"
|
BANK_URL="${BANK_URL:-http://127.0.0.1:9012}"
|
||||||
BANK_PUBLIC_URL="${BANK_PUBLIC_URL:-https://bank.hacktivism.ch}"
|
BANK_PUBLIC_URL="${BANK_PUBLIC_URL:-https://bank.hacktivism.ch}"
|
||||||
EXCHANGE_CONFIG_URL="${EXCHANGE_CONFIG_URL:-https://exchange.hacktivism.ch/config}"
|
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}"
|
BANK_LANDING_IN="${BANK_LANDING_IN:-/var/www/bank-landing}"
|
||||||
EX_LANDING_IN="${EX_LANDING_IN:-/var/www/exchange-landing}"
|
EX_LANDING_IN="${EX_LANDING_IN:-/var/www/exchange-landing}"
|
||||||
MER_LANDING_IN="${MER_LANDING_IN:-/var/www/merchant-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}"
|
WORKDIR="${LANDING_STATS_WORKDIR:-${XDG_RUNTIME_DIR:-/tmp}/taler-landing-stats}"
|
||||||
mkdir -p "$WORKDIR"
|
mkdir -p "$WORKDIR"
|
||||||
|
|
@ -100,9 +64,7 @@ MEM_SRC="${MEM_SNAPSHOT_SRC:-$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh}"
|
||||||
|
|
||||||
merge_container_resources() {
|
merge_container_resources() {
|
||||||
local label="$1" ctr="$2" stats_file="$3"
|
local label="$1" ctr="$2" stats_file="$3"
|
||||||
[ "${COLLECT_RESOURCES}" = "1" ] || return 0
|
|
||||||
[ -f "$stats_file" ] || return 0
|
[ -f "$stats_file" ] || return 0
|
||||||
[ -n "$ctr" ] || return 0
|
|
||||||
if ! ctr_running "$ctr"; then
|
if ! ctr_running "$ctr"; then
|
||||||
log "WARN: $label resources: $ctr not running"
|
log "WARN: $label resources: $ctr not running"
|
||||||
return 1
|
return 1
|
||||||
|
|
@ -133,36 +95,6 @@ merge_container_resources() {
|
||||||
return 0
|
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() {
|
read_pass() {
|
||||||
local name="$1" f
|
local name="$1" f
|
||||||
for f in \
|
for f in \
|
||||||
|
|
@ -175,26 +107,12 @@ read_pass() {
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
# container secrets: /root/… (GOA) or /data/secrets/… (FrancPaysan stage)
|
# container (hernani can podman exec root files inside owned containers)
|
||||||
if [ -n "${BANK_CTR:-}" ] && podman inspect -f '{{.State.Running}}' "$BANK_CTR" 2>/dev/null | grep -qx true; then
|
if 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
|
if podman exec "$BANK_CTR" test -r "/root/${name}" 2>/dev/null; then
|
||||||
case "$path" in
|
podman exec "$BANK_CTR" cat "/root/${name}" 2>/dev/null | tr -d '\n'
|
||||||
*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
|
return 0
|
||||||
fi
|
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
|
fi
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
@ -205,8 +123,6 @@ ctr_running() {
|
||||||
|
|
||||||
publish_json() {
|
publish_json() {
|
||||||
local ctr="$1" dest_dir="$2" src_stats="$3" src_run="$4"
|
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
|
if ! ctr_running "$ctr"; then
|
||||||
log "WARN: $ctr not running — skip publish $dest_dir"
|
log "WARN: $ctr not running — skip publish $dest_dir"
|
||||||
return 1
|
return 1
|
||||||
|
|
@ -250,7 +166,6 @@ collect_bank() {
|
||||||
BANK_URL="$BANK_URL" \
|
BANK_URL="$BANK_URL" \
|
||||||
BANK_PUBLIC_URL="$BANK_PUBLIC_URL" \
|
BANK_PUBLIC_URL="$BANK_PUBLIC_URL" \
|
||||||
EXCHANGE_CONFIG_URL="$EXCHANGE_CONFIG_URL" \
|
EXCHANGE_CONFIG_URL="$EXCHANGE_CONFIG_URL" \
|
||||||
BANK_CURRENCY="${BANK_CURRENCY:-}" \
|
|
||||||
BANK_ADMIN_PASS="$admin_pass" \
|
BANK_ADMIN_PASS="$admin_pass" \
|
||||||
BANK_EXPLORER_PASS="$explorer_pass" \
|
BANK_EXPLORER_PASS="$explorer_pass" \
|
||||||
DEMO_DIR="${demo_dir}" \
|
DEMO_DIR="${demo_dir}" \
|
||||||
|
|
@ -258,7 +173,6 @@ collect_bank() {
|
||||||
TX_PAGE="${TX_PAGE:-500}" \
|
TX_PAGE="${TX_PAGE:-500}" \
|
||||||
MAX_TX_PAGES="${MAX_TX_PAGES:-0}" \
|
MAX_TX_PAGES="${MAX_TX_PAGES:-0}" \
|
||||||
ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--10000}" \
|
ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--10000}" \
|
||||||
STATS_SOURCE_LABEL="$STATS_SOURCE_LABEL" \
|
|
||||||
"$PY" "$LIB/collect_bank_stats.py" \
|
"$PY" "$LIB/collect_bank_stats.py" \
|
||||||
--out "$WORKDIR/bank-stats.json" \
|
--out "$WORKDIR/bank-stats.json" \
|
||||||
--run-out "$WORKDIR/bank-stats-run.json" \
|
--run-out "$WORKDIR/bank-stats-run.json" \
|
||||||
|
|
@ -267,13 +181,11 @@ collect_bank() {
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
if [ "$ec_bank" -eq 0 ] && [ -f "$WORKDIR/bank-stats.json" ]; then
|
if [ "$ec_bank" -eq 0 ] && [ -f "$WORKDIR/bank-stats.json" ]; then
|
||||||
# Optional: attach in-container RSS/loadavg (not host /proc)
|
# Always attach in-container RSS/loadavg (not host /proc)
|
||||||
merge_container_resources bank "$BANK_CTR" "$WORKDIR/bank-stats.json" || true
|
merge_container_resources bank "$BANK_CTR" "$WORKDIR/bank-stats.json" || true
|
||||||
publish_json "$BANK_CTR" "$BANK_LANDING_IN" \
|
publish_json "$BANK_CTR" "$BANK_LANDING_IN" \
|
||||||
"$WORKDIR/bank-stats.json" "$WORKDIR/bank-stats-run.json"
|
"$WORKDIR/bank-stats.json" "$WORKDIR/bank-stats-run.json"
|
||||||
# Shared public feed: same bank stats on all HOST_STATS_DIRS (bank/ex/merchant landings)
|
log "bank: OK → ${BANK_CTR}:${BANK_LANDING_IN}/stats.json"
|
||||||
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
|
else
|
||||||
log "ERROR: bank collect failed (ec=$ec_bank) — previous stats.json kept"
|
log "ERROR: bank collect failed (ec=$ec_bank) — previous stats.json kept"
|
||||||
[ -f "$WORKDIR/bank-stats-run.json" ] || \
|
[ -f "$WORKDIR/bank-stats-run.json" ] || \
|
||||||
|
|
@ -360,26 +272,13 @@ collect_merchant() {
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
main() {
|
main() {
|
||||||
log "=== taler-landing-stats start (user=$(id -un) lib=$LIB currency=${BANK_CURRENCY:-auto}) ==="
|
log "=== taler-landing-stats start (user=$(id -un) lib=$LIB) ==="
|
||||||
log "phases bank=${COLLECT_BANK} exchange=${COLLECT_EXCHANGE} merchant=${COLLECT_MERCHANT} resources=${COLLECT_RESOURCES}"
|
|
||||||
if [ "${COLLECT_BANK}" = "1" ]; then
|
|
||||||
collect_bank || ec_bank=$?
|
collect_bank || ec_bank=$?
|
||||||
else
|
|
||||||
log "skip bank collect"
|
|
||||||
fi
|
|
||||||
if [ "${COLLECT_EXCHANGE}" = "1" ]; then
|
|
||||||
collect_exchange || ec_ex=$?
|
collect_exchange || ec_ex=$?
|
||||||
else
|
|
||||||
log "skip exchange collect"
|
|
||||||
fi
|
|
||||||
if [ "${COLLECT_MERCHANT}" = "1" ]; then
|
|
||||||
collect_merchant || ec_mer=$?
|
collect_merchant || ec_mer=$?
|
||||||
else
|
|
||||||
log "skip merchant collect"
|
|
||||||
fi
|
|
||||||
log "=== done bank=$ec_bank exchange=$ec_ex merchant=$ec_mer ==="
|
log "=== done bank=$ec_bank exchange=$ec_ex merchant=$ec_mer ==="
|
||||||
# non-zero if bank failed (primary public flow numbers); soft on ex/mer
|
# non-zero if bank failed (primary public flow numbers); soft on ex/mer
|
||||||
if [ "${COLLECT_BANK}" = "1" ] && [ "$ec_bank" -ne 0 ]; then
|
if [ "$ec_bank" -ne 0 ]; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Full-scan bank landing stats (all accounts, all ledger money).
|
"""Full-scan bank landing stats (all accounts, all ledger money).
|
||||||
|
|
||||||
Generic for any currency stack (GOA, TESTPAYSAN, KUDOS, …). Run on the ops host
|
Run on koopa as hernani (or anywhere with admin API access). Writes stats.json
|
||||||
(with admin API access). Writes stats.json for public landings + amount_alt.
|
compatible with bank.hacktivism.ch/intro/ plus amount_alt fields for compact UI.
|
||||||
|
|
||||||
Env (selected):
|
Env (selected):
|
||||||
BANK_URL default http://127.0.0.1:9012
|
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_USER default admin
|
||||||
BANK_ADMIN_PASS or BANK_ADMIN_PASS_FILE / pass via --admin-pass-file
|
BANK_ADMIN_PASS or BANK_ADMIN_PASS_FILE / pass via --admin-pass-file
|
||||||
BANK_EXPLORER_USER default explorer
|
BANK_EXPLORER_USER default explorer
|
||||||
BANK_EXPLORER_PASS optional (balance_explorer)
|
BANK_EXPLORER_PASS optional (balance_explorer)
|
||||||
EXCHANGE_CONFIG_URL for alt_unit_names + currency
|
EXCHANGE_CONFIG_URL for alt_unit_names (default https://exchange.hacktivism.ch/config)
|
||||||
BANK_PUBLIC_URL public base for latency probes
|
|
||||||
OUT output path (default stdout if -)
|
OUT output path (default stdout if -)
|
||||||
SCAN_SKIP comma usernames excluded from flow (default: exchange)
|
SCAN_SKIP comma usernames excluded from flow (default: exchange)
|
||||||
TX_PAGE / MAX_TX_PAGES / ACCOUNTS_DELTA / RECENT_WD_N / TZ
|
TX_PAGE page size for transactions delta (default 500)
|
||||||
STATS_SOURCE_LABEL optional string in stats.source field
|
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
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -342,11 +343,6 @@ def main() -> int:
|
||||||
"--exchange-config",
|
"--exchange-config",
|
||||||
default=env("EXCHANGE_CONFIG_URL", "https://exchange.hacktivism.ch/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("--out", default=env("OUT", "-"))
|
||||||
ap.add_argument("--run-out", default=env("RUN_OUT", ""))
|
ap.add_argument("--run-out", default=env("RUN_OUT", ""))
|
||||||
ap.add_argument(
|
ap.add_argument(
|
||||||
|
|
@ -368,11 +364,6 @@ def main() -> int:
|
||||||
)
|
)
|
||||||
ap.add_argument("--recent", type=int, default=int(env("RECENT_WD_N", "10") or "10"))
|
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("--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()
|
args = ap.parse_args()
|
||||||
|
|
||||||
tz_name = env("TZ", "Europe/Zurich") or "Europe/Zurich"
|
tz_name = env("TZ", "Europe/Zurich") or "Europe/Zurich"
|
||||||
|
|
@ -387,7 +378,6 @@ def main() -> int:
|
||||||
|
|
||||||
bank = args.bank.rstrip("/")
|
bank = args.bank.rstrip("/")
|
||||||
skip = {s.strip() for s in (args.skip or "").split(",") if s.strip()}
|
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:
|
def write_run(ok: bool, err: Optional[str] = None) -> None:
|
||||||
if not args.run_out:
|
if not args.run_out:
|
||||||
|
|
@ -407,24 +397,6 @@ def main() -> int:
|
||||||
raise RuntimeError("no admin password (BANK_ADMIN_PASS or --admin-pass-file)")
|
raise RuntimeError("no admin password (BANK_ADMIN_PASS or --admin-pass-file)")
|
||||||
token = get_token(bank, args.admin_user, admin_pass)
|
token = get_token(bank, args.admin_user, admin_pass)
|
||||||
alt = load_alt_from_config(args.exchange_config)
|
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:
|
if not alt:
|
||||||
alt = dict(DEFAULT_ALT)
|
alt = dict(DEFAULT_ALT)
|
||||||
|
|
||||||
|
|
@ -470,24 +442,13 @@ def main() -> int:
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
low = (subject or "").lower()
|
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":
|
if direction == "credit":
|
||||||
total_in += n
|
total_in += n
|
||||||
n_incoming += 1
|
n_incoming += 1
|
||||||
incomings.append(
|
incomings.append(
|
||||||
{
|
{
|
||||||
"kind": "incoming",
|
"kind": "incoming",
|
||||||
"amount": amount,
|
"amount": amount if ":" in amount else f"GOA:{amount}",
|
||||||
"at_unix": ts,
|
"at_unix": ts,
|
||||||
"account": uname,
|
"account": uname,
|
||||||
"subject": subject,
|
"subject": subject,
|
||||||
|
|
@ -499,7 +460,7 @@ def main() -> int:
|
||||||
withdraws.append(
|
withdraws.append(
|
||||||
{
|
{
|
||||||
"kind": "withdraw",
|
"kind": "withdraw",
|
||||||
"amount": amount,
|
"amount": amount if ":" in amount else f"GOA:{amount}",
|
||||||
"at_unix": ts,
|
"at_unix": ts,
|
||||||
"account": uname,
|
"account": uname,
|
||||||
"subject": subject,
|
"subject": subject,
|
||||||
|
|
@ -542,7 +503,7 @@ def main() -> int:
|
||||||
users_n = sum(1 for u in accounts if u not in ("admin", "exchange"))
|
users_n = sum(1 for u in accounts if u not in ("admin", "exchange"))
|
||||||
|
|
||||||
def pack_amt(d: Decimal) -> Dict[str, Any]:
|
def pack_amt(d: Decimal) -> Dict[str, Any]:
|
||||||
f = format_amount_alt(f"{currency}:{d}", alt)
|
f = format_amount_alt(f"GOA:{d}", alt)
|
||||||
return {
|
return {
|
||||||
"amount": f["amount"],
|
"amount": f["amount"],
|
||||||
"amount_alt": f["amount_alt"],
|
"amount_alt": f["amount_alt"],
|
||||||
|
|
@ -606,11 +567,10 @@ def main() -> int:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# explorer / pool balance (shared demo account when present)
|
# explorer balance
|
||||||
zero = f"{currency}:0"
|
balance = "GOA:0"
|
||||||
balance = zero
|
bal_alt = "GOA:0"
|
||||||
bal_alt = zero
|
bal_full = "GOA:0"
|
||||||
bal_full = zero
|
|
||||||
if explorer_pass:
|
if explorer_pass:
|
||||||
try:
|
try:
|
||||||
etok = get_token(bank, args.explorer_user, explorer_pass)
|
etok = get_token(bank, args.explorer_user, explorer_pass)
|
||||||
|
|
@ -623,7 +583,7 @@ def main() -> int:
|
||||||
balance = str(
|
balance = str(
|
||||||
data.get("balance", {}).get("amount")
|
data.get("balance", {}).get("amount")
|
||||||
if isinstance(data.get("balance"), dict)
|
if isinstance(data.get("balance"), dict)
|
||||||
else data.get("amount") or data.get("balance") or zero
|
else data.get("amount") or data.get("balance") or "GOA:0"
|
||||||
)
|
)
|
||||||
# sometimes balance is object with amount
|
# sometimes balance is object with amount
|
||||||
if isinstance(data.get("balance"), dict) and data["balance"].get(
|
if isinstance(data.get("balance"), dict) and data["balance"].get(
|
||||||
|
|
@ -648,7 +608,7 @@ def main() -> int:
|
||||||
)
|
)
|
||||||
if code == 200 and isinstance(data, dict):
|
if code == 200 and isinstance(data, dict):
|
||||||
if isinstance(data.get("balance"), dict):
|
if isinstance(data.get("balance"), dict):
|
||||||
balance = str(data["balance"].get("amount") or zero)
|
balance = str(data["balance"].get("amount") or "GOA:0")
|
||||||
elif data.get("amount"):
|
elif data.get("amount"):
|
||||||
balance = str(data["amount"])
|
balance = str(data["amount"])
|
||||||
bf = format_amount_alt(balance, alt)
|
bf = format_amount_alt(balance, alt)
|
||||||
|
|
@ -677,12 +637,12 @@ def main() -> int:
|
||||||
|
|
||||||
stats = {
|
stats = {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"currency": currency,
|
"currency": "GOA",
|
||||||
"timezone": tz_name,
|
"timezone": tz_name,
|
||||||
"generated_at": gen_iso,
|
"generated_at": gen_iso,
|
||||||
"generated_at_human": gen_human,
|
"generated_at_human": gen_human,
|
||||||
"generated_at_unix": now_u,
|
"generated_at_unix": now_u,
|
||||||
"source": str(args.source_label or "host collect_bank_stats.py"),
|
"source": "host collect_bank_stats.py (hernani systemd)",
|
||||||
"bank_url": bank,
|
"bank_url": bank,
|
||||||
"scan": {
|
"scan": {
|
||||||
"tx_page": args.tx_page,
|
"tx_page": args.tx_page,
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,35 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Install generic landing-stats collector as user systemd timer.
|
# Install central landing-stats collector as hernani user systemd timer.
|
||||||
#
|
#
|
||||||
# GOA / koopa (default):
|
# On koopa:
|
||||||
# cd ~/src/koopa/koopa-admin-log
|
# cd ~/src/koopa/koopa-admin-log
|
||||||
# ./scripts/taler-landing/install-landing-stats-host.sh
|
# ./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
|
||||||
#
|
#
|
||||||
# Stage TESTPAYSAN (stagepaysan on francpaysan host):
|
# Requires: linger enabled for hernani (loginctl enable-linger hernani)
|
||||||
# STACK_PROFILE=stage-testpaysan ./scripts/taler-landing/install-landing-stats-host.sh
|
# so the timer runs without an interactive login.
|
||||||
# # 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
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
ADMIN_LOG="${ADMIN_LOG:-$ROOT}"
|
ADMIN_LOG="${ADMIN_LOG:-$ROOT}"
|
||||||
SRC="$ADMIN_LOG/scripts/taler-landing"
|
SRC="$ADMIN_LOG/scripts/taler-landing"
|
||||||
UNIT_SRC="$ADMIN_LOG/configs/systemd/user"
|
UNIT_SRC="$ADMIN_LOG/configs/systemd/user"
|
||||||
PROFILE="${STACK_PROFILE:-goa}"
|
|
||||||
|
|
||||||
BIN_DST="${HOME}/.local/bin"
|
BIN_DST="${HOME}/.local/bin"
|
||||||
LIB_DST="${HOME}/.local/lib/taler-landing"
|
LIB_DST="${HOME}/.local/lib/taler-landing"
|
||||||
UNIT_DST="${HOME}/.config/systemd/user"
|
UNIT_DST="${HOME}/.config/systemd/user"
|
||||||
STATE_DST="${HOME}/.local/state/taler-landing-stats"
|
STATE_DST="${HOME}/.local/state/taler-landing-stats"
|
||||||
CFG_DST="${HOME}/.config/taler-landing"
|
|
||||||
|
|
||||||
mkdir -p "$BIN_DST" "$LIB_DST" "$UNIT_DST" "$STATE_DST" "$CFG_DST"
|
mkdir -p "$BIN_DST" "$LIB_DST" "$UNIT_DST" "$STATE_DST"
|
||||||
|
|
||||||
install -m 0755 "$SRC/collect-landing-stats.sh" "$BIN_DST/collect-landing-stats.sh"
|
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/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/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/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/collect_container_resources.sh" "$LIB_DST/collect_container_resources.sh"
|
||||||
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" "$BIN_DST/test-landing-stats.sh" 2>/dev/null || \
|
|
||||||
install -m 0755 "$SRC/test-landing-stats.sh" "$LIB_DST/test-landing-stats.sh"
|
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"
|
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)
|
# mem-snapshot helper lives in taler-shared (copied into containers at collect time)
|
||||||
if [ -f "$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh" ]; then
|
if [ -f "$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh" ]; then
|
||||||
|
|
@ -43,34 +37,18 @@ if [ -f "$ADMIN_LOG/scripts/taler-shared/mem-snapshot.sh" ]; then
|
||||||
"$LIB_DST/mem-snapshot.sh"
|
"$LIB_DST/mem-snapshot.sh"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Stack profile → ~/.config/taler-landing/stack.env (sourced by collector)
|
# Wrapper always uses installed lib next to itself when LIB discovery works;
|
||||||
if [ -n "${STACK_ENV_FILE:-}" ] && [ -f "${STACK_ENV_FILE}" ]; then
|
# also point ADMIN_LOG for in-container script refresh.
|
||||||
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.service" "$UNIT_DST/"
|
||||||
install -m 0644 "$UNIT_SRC/taler-landing-stats.timer" "$UNIT_DST/"
|
install -m 0644 "$UNIT_SRC/taler-landing-stats.timer" "$UNIT_DST/"
|
||||||
|
|
||||||
# Point unit EnvironmentFile at stack profile
|
# Rewrite ExecStart to installed binary if unit uses %h path — units already do.
|
||||||
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 daemon-reload
|
||||||
systemctl --user enable --now taler-landing-stats.timer
|
systemctl --user enable --now taler-landing-stats.timer
|
||||||
|
|
||||||
echo "Installed (user=$(id -un) profile=${PROFILE}):"
|
echo "Installed (user=$(id -un)):"
|
||||||
echo " $BIN_DST/collect-landing-stats.sh"
|
echo " $BIN_DST/collect-landing-stats.sh"
|
||||||
echo " $LIB_DST/{collect_bank_stats,enrich_stats_alt,goa_amounts}.py"
|
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 " $UNIT_DST/taler-landing-stats.{service,timer} (timer enabled)"
|
||||||
echo " logs: $STATE_DST/"
|
echo " logs: $STATE_DST/"
|
||||||
echo
|
echo
|
||||||
|
|
@ -83,6 +61,8 @@ echo "Run once now:"
|
||||||
echo " systemctl --user start taler-landing-stats.service"
|
echo " systemctl --user start taler-landing-stats.service"
|
||||||
echo " journalctl --user -u taler-landing-stats.service -n 40 --no-pager"
|
echo " journalctl --user -u taler-landing-stats.service -n 40 --no-pager"
|
||||||
echo
|
echo
|
||||||
echo "Secrets (if not via podman exec bank):"
|
echo "Optional secrets (if not readable via podman exec bank /root/…):"
|
||||||
echo " mkdir -p ~/.config/taler-landing && chmod 700 ~/.config/taler-landing"
|
echo " mkdir -p ~/.config/taler-landing"
|
||||||
echo " # bank-admin-password.txt bank-explorer-password.txt"
|
echo " cp …/bank-admin-password.txt ~/.config/taler-landing/"
|
||||||
|
echo " cp …/bank-explorer-password.txt ~/.config/taler-landing/"
|
||||||
|
echo " chmod 600 ~/.config/taler-landing/*"
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
# 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"
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
# 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,7 +363,6 @@ CUR_JSON="${CUR_JSON}
|
||||||
|
|
||||||
GEN=$(now_iso)
|
GEN=$(now_iso)
|
||||||
HUMAN=$(now_human)
|
HUMAN=$(now_human)
|
||||||
GEN_UNIX=$(date +%s)
|
|
||||||
|
|
||||||
# Live performance probes (exchange-style)
|
# Live performance probes (exchange-style)
|
||||||
measure_ms() {
|
measure_ms() {
|
||||||
|
|
@ -407,7 +406,6 @@ cat >"$TMP" <<EOF
|
||||||
"timezone": $(json_str "$TZ"),
|
"timezone": $(json_str "$TZ"),
|
||||||
"generated_at": $(json_str "$GEN"),
|
"generated_at": $(json_str "$GEN"),
|
||||||
"generated_at_human": $(json_str "$HUMAN"),
|
"generated_at_human": $(json_str "$HUMAN"),
|
||||||
"generated_at_unix": ${GEN_UNIX},
|
|
||||||
"instances": ${INSTANCES:-0},
|
"instances": ${INSTANCES:-0},
|
||||||
"orders": ${TOTAL_CONTRACTS:-0},
|
"orders": ${TOTAL_CONTRACTS:-0},
|
||||||
"paid": ${TOTAL_PAID:-0},
|
"paid": ${TOTAL_PAID:-0},
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,24 @@
|
||||||
# taler-monitoring
|
# taler-monitoring
|
||||||
|
|
||||||
Report for the **GOA** stack with boxed severity badges and **grouped test IDs**
|
Report for the **GOA** stack with clear severity tags and **per-area test IDs**:
|
||||||
`area.group-NN` (e.g. `www.exchange-01`, `e2e.pay-03`):
|
|
||||||
|
|
||||||
| Tag | Meaning |
|
| Tag | Meaning |
|
||||||
|-----|---------|
|
|-----|---------|
|
||||||
| `┌ OK ┐` | check passed |
|
| `[OK]` | check passed |
|
||||||
| `┌ INFO ┐` | status / note |
|
| `[INFO]` | inside status (container, ports, log noise) |
|
||||||
| `┌ WARN ┐` | degraded but maybe not fatal |
|
| `[WARN]` | degraded but maybe not fatal |
|
||||||
| `┌ ERROR ┐` | component problem |
|
| `[ERROR]` | component problem |
|
||||||
| `┌ BLOCKER ┐` | **withdraw/pay path cannot complete** because of this |
|
| `[BLOCKER]` | **withdraw/pay path cannot complete** because of this |
|
||||||
|
|
||||||
IDs reset per **group** inside an area — so “too many www tests” become
|
IDs: **`www-001`**, **`inside-001`**, **`versions-001`**, **`sanity-001`**, **`server-001`**, **`e2e-001`** …
|
||||||
`www.exchange-*`, `www.bank-*`, `www.landing-*`, etc.
|
|
||||||
Catalog: **[TESTS.md](./TESTS.md)**.
|
Catalog: **[TESTS.md](./TESTS.md)**.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
┌ OK ┐ www.exchange-01 exchange /config · HTTP 200
|
[OK] www-001 exchange /config https://exchange…/config
|
||||||
┌ BLOCKER┐ e2e.prereq-02 merchant HTTP 502
|
[BLOCKER] e2e-015 prereq: merchant HTTP 502
|
||||||
```
|
```
|
||||||
|
|
||||||
End of each phase: totals + list of **BLOCKERS** and **ERRORS** (ids included).
|
End of each phase: totals + list of **BLOCKERS** and **ERRORS**.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
|
@ -43,27 +41,8 @@ End of each phase: totals + list of **BLOCKERS** and **ERRORS** (ids included).
|
||||||
./taler-monitoring.sh --bank https://bank.demo.taler.net \
|
./taler-monitoring.sh --bank https://bank.demo.taler.net \
|
||||||
--exchange https://exchange.demo.taler.net \
|
--exchange https://exchange.demo.taler.net \
|
||||||
--merchant https://backend.demo.taler.net --currency KUDOS urls
|
--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`)
|
## Domain profiles (`domains.conf`)
|
||||||
|
|
||||||
Each monitored stack must say **what is bank, exchange, and merchant-backend**.
|
Each monitored stack must say **what is bank, exchange, and merchant-backend**.
|
||||||
|
|
|
||||||
|
|
@ -1,132 +1,217 @@
|
||||||
# taler-monitoring — grouped test IDs
|
# taler-monitoring — test IDs by area
|
||||||
|
|
||||||
Every check line is numbered **`area.group-NN`** (two-digit group counter):
|
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:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
┌ OK ┐ www.exchange-01 exchange /config · HTTP 200
|
[OK] www-001 exchange /config https://exchange…/config
|
||||||
┌ ERROR ┐ e2e.pay-03 order create failed · HTTP 502
|
[ERROR] e2e-012 bank-auth: admin token failed
|
||||||
┌ BLOCKER┐ e2e.prereq-02 merchant secret missing
|
[BLOCKER] e2e-015 prereq: merchant HTTP 502
|
||||||
```
|
```
|
||||||
|
|
||||||
| Area | Phase script | Groups (examples) |
|
IDs are assigned **in run order** within the area (`set_area` resets the counter). Optional soft checks still consume a number when they WARN.
|
||||||
|------|--------------|-------------------|
|
|
||||||
| **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`)
|
## www — public URLs (`./taler-monitoring.sh urls`)
|
||||||
|
|
||||||
| Group | Checks (typical) |
|
| ID | Check |
|
||||||
|-------|------------------|
|
|----|--------|
|
||||||
| **www.exchange-** | `/config`, currency, **alt_unit_names**; `/keys` (+ alt soft); `/intro/`, `/`; **`/terms`**, **`/privacy`**, `/terms/` |
|
| www-… | exchange `/config`, currency, **alt_unit_names** |
|
||||||
| **www.perf-** | outside-in RTT: bank/exchange/merchant `/config`, keys, webui, intro — ms; WARN ≥ `PERF_WARN_MS` (8000); ERROR ≥ `PERF_FAIL_MS` (20000) |
|
| www-… | exchange `/keys` (+ alt_unit_names soft) |
|
||||||
| **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-… | exchange `/intro/`, `/` (302→intro) |
|
||||||
| **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-… | **exchange `/terms`** body (not empty / not API error) |
|
||||||
| **www.merchant-** | `/config` currency + currencies alt_unit_names; listed exchanges alt; webui/intro; **`/terms`**, **`/privacy`** |
|
| www-… | **exchange `/privacy`** body |
|
||||||
| **www.paivana-** | local GOA paywall front (redirect to template) |
|
| www-… | exchange `/terms/` (200 or redirect) |
|
||||||
| **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 |
|
| 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 |
|
||||||
|
|
||||||
**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…).
|
**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…).
|
||||||
|
|
||||||
**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).
|
**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.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**alt_unit_names rule:** non-empty map including scale key `"0"`. Multi-currency merchant: follow each `exchanges[]` public `/config`.
|
**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.)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## inside — koopa SSH (`./taler-monitoring.sh inside`)
|
## inside — koopa SSH (`./taler-monitoring.sh inside`)
|
||||||
|
|
||||||
| Group | Checks |
|
| ID | Check (typical order) |
|
||||||
|-------|--------|
|
|----|------------------------|
|
||||||
| **inside.ssh-** | ssh reachability / remote collect |
|
| inside-001 | ssh koopa (or **koopa-external** fallback) |
|
||||||
| **inside.bank-** | container, libeufin, postgres, local `/config`, nginx, DNS pins |
|
| inside-002+ | per-component emit: container, ports, libeufin/httpd, postgres, local `/config`/`/keys`, wirewatch, DNS pin, caddy |
|
||||||
| **inside.exchange-** | container, httpd, wirewatch, aggregator, transfer, local keys, DNS |
|
| inside-… | **load / memory**: host loadavg + RAM; bank/exchange/merchant podman CPU/mem/block + process RSS by role + DB sizes |
|
||||||
| **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 under that component group.
|
Remote lines `E|comp|LEVEL|key|detail` each become one numbered result.
|
||||||
|
|
||||||
SSH: `KOOPA_SSH` (default `koopa`), then `KOOPA_SSH_FALLBACKS` when LAN is unreachable.
|
SSH: `KOOPA_SSH` (default `koopa`), then `KOOPA_SSH_FALLBACKS` (default `koopa-external`) when LAN is unreachable.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## e2e — withdraw + pay (`./taler-monitoring.sh e2e`)
|
## sanity — bank · exchange · merchant (`./taler-monitoring.sh sanity`)
|
||||||
|
|
||||||
| Group | Checks |
|
| ID | Section |
|
||||||
|-------|--------|
|
|----|---------|
|
||||||
| **e2e.prereq-** | wallet-cli, currency, budgets, secrets, public reachability |
|
| sanity-001… | bank public + server |
|
||||||
| **e2e.load-** | host/container load snapshots (before / after ATM / after pay) |
|
| sanity-… | exchange public + server |
|
||||||
| **e2e.bank-** | account, credit, withdraw ops |
|
| sanity-… | merchant public + server |
|
||||||
| **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 |
|
|
||||||
|
|
||||||
When filing an issue, quote the full id + label, e.g.
|
Sequential through the whole script (one `set_area sanity`).
|
||||||
`e2e.atm-03 ATM withdraw 5 GOA · bank confirmed, wallet empty`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## versions / sanity / ladder / server
|
## versions — packages vs deb.taler.net (`./taler-monitoring.sh versions`)
|
||||||
|
|
||||||
| Area.group | Meaning |
|
### Outside (runner / public network — no SSH)
|
||||||
|------------|---------|
|
|
||||||
| **versions.outside-** | deb.taler.net suite index |
|
| ID (order) | Check |
|
||||||
| **versions.inside-** | packages in containers |
|
|------------|--------|
|
||||||
| **versions.compare-** | installed vs suite |
|
| versions-… | DNS `deb.taler.net` |
|
||||||
| **sanity.bank-** / **.exchange-** / **.merchant-** | public + optional server-side per component |
|
| versions-… | HTTPS portal + apt base URL |
|
||||||
| **ladder.plan-** / **.load-** / **.withdraw-** / **.pay-** / **.report-** | GOA amount ladder |
|
| versions-… | suite `InRelease` / `Release` |
|
||||||
| **server-** | SSH host ports / processes (flat unless grouped later) |
|
| 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Visual format (colour on)
|
## 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):
|
||||||
|
|
||||||
```text
|
```text
|
||||||
╔══════════════════════════════╗
|
withdraw: [0] + random mids + [max−1] + [max]
|
||||||
║ www · public URLs · … ║
|
pay: [0] + (withdraw_mid / scale) + [max−1] + [max] # always ≤ matching withdraw mid
|
||||||
╚══════════════════════════════╝
|
|
||||||
┌ 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`NO_COLOR=1` or `CLICOLOR=0` disables boxes/colours (ASCII `[ OK ]` + `-- group --` headers).
|
TSV `range`: `pin:0` | `random` | `pin:max-1` | `pin:max`.
|
||||||
Always use `printf --` friendly plain headers when colour is off (leading `---` is not a printf option).
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -23,37 +23,19 @@ e2e_over() { [ "$(e2e_left)" -le 0 ]; }
|
||||||
# stdout only for JSON; logs go to stderr file so balance parse stays clean
|
# stdout only for JSON; logs go to stderr file so balance parse stays clean
|
||||||
wcli_bal_snap() {
|
wcli_bal_snap() {
|
||||||
local out="$1"
|
local out="$1"
|
||||||
if [ -z "${WALLET_CLI:-}" ]; then
|
if [ -z "${WALLET_CLI:-}" ] || [ ! -f "${WDB:-}" ]; then
|
||||||
echo "(no wallet)" >"$out"
|
echo "(no wallet)" >"$out"
|
||||||
return 1
|
return 1
|
||||||
fi
|
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"
|
local err="${out}.err"
|
||||||
if command -v perl >/dev/null 2>&1; then
|
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 \
|
perl -e 'alarm shift; exec @ARGV' 10 \
|
||||||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \
|
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \
|
||||||
>"$out" 2>"$err" || true
|
>"$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
|
else
|
||||||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \
|
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults balance \
|
||||||
>"$out" 2>"$err" || true
|
>"$out" 2>"$err" || true
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
[ -s "$out" ]
|
[ -s "$out" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,9 +104,9 @@ wait_wallet_balance() {
|
||||||
local rounds="${2:-15}"
|
local rounds="${2:-15}"
|
||||||
local sleep_s="${3:-3}"
|
local sleep_s="${3:-3}"
|
||||||
local r av
|
local r av
|
||||||
# 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} (wirewatch lag is common)"
|
||||||
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
|
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
|
wcli_bal_snap "$SCRATCH/bal-live.out" || wcli balance >"$SCRATCH/bal-live.out" 2>&1 || true
|
||||||
av=$(wallet_avail_num "$SCRATCH/bal-live.out")
|
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
|
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
|
||||||
|
|
@ -200,7 +182,6 @@ e2e_skip_rest() {
|
||||||
|
|
||||||
# Area e2e-### — withdraw + pay cycle
|
# Area e2e-### — withdraw + pay cycle
|
||||||
set_area e2e
|
set_area e2e
|
||||||
set_group prereq
|
|
||||||
section "e2e · prerequisites"
|
section "e2e · prerequisites"
|
||||||
info "e2e budget" "${E2E_TIMEOUT}s (set E2E_TIMEOUT= to change)"
|
info "e2e budget" "${E2E_TIMEOUT}s (set E2E_TIMEOUT= to change)"
|
||||||
|
|
||||||
|
|
@ -239,22 +220,12 @@ except Exception: print("")' 2>/dev/null || true)
|
||||||
if [ "$E2E_REMOTE" = "1" ]; then
|
if [ "$E2E_REMOTE" = "1" ]; then
|
||||||
# remote: still ATM-shaped but smaller notes; keep cheap
|
# remote: still ATM-shaped but smaller notes; keep cheap
|
||||||
: "${E2E_WITHDRAW_VALUES:=10 20 50}"
|
: "${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}"
|
: "${E2E_PAY_VALUES:=0.01 0.05 0.1 1}"
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
# local GOA: classic ATM notes + 4200 for paivana paywall template
|
# local GOA: classic ATM notes + 4200 for paivana paywall template
|
||||||
: "${E2E_WITHDRAW_VALUES:=20 50 100 200 4200}"
|
: "${E2E_WITHDRAW_VALUES:=20 50 100 200 4200}"
|
||||||
: "${E2E_PAY_VALUES:=0.01 0.05 0.1 0.5 1 2 5 10}"
|
: "${E2E_PAY_VALUES:=0.01 0.05 0.1 0.5 1 2 5 10}"
|
||||||
fi
|
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 CUR:amount lists
|
||||||
build_amt_list() {
|
build_amt_list() {
|
||||||
|
|
@ -328,27 +299,12 @@ BANK_HOST=$(python3 -c 'from urllib.parse import urlparse; print(urlparse("'"$BA
|
||||||
|
|
||||||
SCRATCH=$(mktemp -d)
|
SCRATCH=$(mktemp -d)
|
||||||
WDB="$SCRATCH/wallet.sqlite3"
|
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}"
|
METRICS_DIR="${METRICS_DIR:-$SCRATCH/metrics}"
|
||||||
mkdir -p "$METRICS_DIR"
|
mkdir -p "$METRICS_DIR"
|
||||||
export METRICS_DIR CUR="${CUR:-${EXPECT_CURRENCY:-GOA}}" WDB
|
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"
|
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)
|
# Always dump balances on any exit (timeout, blocker, signal, success)
|
||||||
trap 'ec=$?; e2e_finish "$ec"; stop_wallet_serve; rm -rf "$SCRATCH"; exit "$ec"' EXIT
|
trap 'ec=$?; e2e_finish "$ec"; rm -rf "$SCRATCH"; exit "$ec"' EXIT
|
||||||
|
|
||||||
# Abort cleanly on login / KYC / registration barriers (esp. remote domains)
|
# Abort cleanly on login / KYC / registration barriers (esp. remote domains)
|
||||||
e2e_abort_auth() {
|
e2e_abort_auth() {
|
||||||
|
|
@ -426,12 +382,10 @@ else
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Host + bank/exchange/merchant RAM/CPU/load (SSH koopa or koopa-external)
|
# Host + bank/exchange/merchant RAM/CPU/load (SSH koopa or koopa-external)
|
||||||
set_group load
|
|
||||||
section "e2e · load snapshot (before withdraw/pay)"
|
section "e2e · load snapshot (before withdraw/pay)"
|
||||||
metrics_report_load "${METRICS_DIR}/load-before.json" "e2e-start" || true
|
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).
|
# Local stack: koopa secrets. Remote: only explicit env (never leak local passwords to foreign banks).
|
||||||
set_group prereq
|
|
||||||
if [ "$E2E_REMOTE" = "1" ]; then
|
if [ "$E2E_REMOTE" = "1" ]; then
|
||||||
ADMIN_PASS="${E2E_BANK_ADMIN_PASS:-}"
|
ADMIN_PASS="${E2E_BANK_ADMIN_PASS:-}"
|
||||||
MPW="${E2E_MERCHANT_TOKEN:-${MERCHANT_TOKEN:-}}"
|
MPW="${E2E_MERCHANT_TOKEN:-${MERCHANT_TOKEN:-}}"
|
||||||
|
|
@ -448,29 +402,13 @@ else
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ -n "$MPW" ]; then
|
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})"
|
ok "merchant secret (instance ${MERCHANT_INSTANCE})"
|
||||||
elif [ "$E2E_REMOTE" = "1" ]; then
|
elif [ "$E2E_REMOTE" = "1" ]; then
|
||||||
MPW_TOKEN=""
|
warn "merchant secret" "missing — order create may fail (set E2E_MERCHANT_TOKEN)"
|
||||||
warn "merchant secret" "missing — private orders fail unless E2E_USE_TEMPLATES=1 (set E2E_MERCHANT_TOKEN)"
|
|
||||||
else
|
else
|
||||||
blocker "prereq" "merchant instance password missing"
|
blocker "prereq" "merchant instance password missing"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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)
|
# Public reachability gates (remote: soft-skip if bank/merchant absent)
|
||||||
for pair in \
|
for pair in \
|
||||||
|
|
@ -514,31 +452,7 @@ info "e2e user" "$USER withdraw=$WITHDRAW_AMT pay=$PAY_AMT"
|
||||||
BANK="$BANK_PUBLIC"
|
BANK="$BANK_PUBLIC"
|
||||||
INST="$MERCHANT_INSTANCE"
|
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
|
# $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() {
|
wcli() {
|
||||||
local maxc=12
|
local maxc=12
|
||||||
if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
|
if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
|
||||||
|
|
@ -550,21 +464,12 @@ wcli() {
|
||||||
cap=$(e2e_left)
|
cap=$(e2e_left)
|
||||||
[ "$cap" -gt "$maxc" ] && cap=$maxc
|
[ "$cap" -gt "$maxc" ] && cap=$maxc
|
||||||
[ "$cap" -lt 3 ] && return 124
|
[ "$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
|
if command -v perl >/dev/null 2>&1; then
|
||||||
perl -e 'alarm shift; exec @ARGV' "$cap" \
|
perl -e 'alarm shift; exec @ARGV' "$cap" \
|
||||||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
|
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
|
||||||
else
|
else
|
||||||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
|
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Pay must not be starved: use reserved seconds even if global budget is tight
|
# Pay must not be starved: use reserved seconds even if global budget is tight
|
||||||
|
|
@ -581,26 +486,16 @@ wcli_pay() {
|
||||||
cap=12
|
cap=12
|
||||||
fi
|
fi
|
||||||
info "pay wallet cap" "${cap}s"
|
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
|
if command -v perl >/dev/null 2>&1; then
|
||||||
perl -e 'alarm shift; exec @ARGV' "$cap" \
|
perl -e 'alarm shift; exec @ARGV' "$cap" \
|
||||||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
|
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
|
||||||
else
|
else
|
||||||
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
|
node "$WALLET_CLI" --wallet-db="$WDB" --no-throttle --skip-defaults "$@"
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# When coins never become available: run inside + targeted bank/exchange dig
|
# When coins never become available: run inside + targeted bank/exchange dig
|
||||||
dig_when_no_coins() {
|
dig_when_no_coins() {
|
||||||
set_group dig
|
|
||||||
section "e2e · coins missing — inside dig"
|
section "e2e · coins missing — inside dig"
|
||||||
info "reason" "wallet empty after withdraw path (often wirewatch timing) — probing bank + exchange"
|
info "reason" "wallet empty after withdraw path (often wirewatch timing) — probing bank + exchange"
|
||||||
if [ -n "${WID:-}" ]; then
|
if [ -n "${WID:-}" ]; then
|
||||||
|
|
@ -685,7 +580,6 @@ BAL_BEFORE=$(fmt_bal "$SCRATCH/bal-before.out")
|
||||||
info "BALANCE before" "$BAL_BEFORE"
|
info "BALANCE before" "$BAL_BEFORE"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
set_group bank
|
|
||||||
section "e2e · bank (account · credit · withdraw)"
|
section "e2e · bank (account · credit · withdraw)"
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
AT=""
|
AT=""
|
||||||
|
|
@ -784,11 +678,8 @@ else
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
set_group wallet
|
|
||||||
section "e2e · wallet setup (exchange + ToS once)"
|
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 ! wcli exchanges add "${EXCHANGE_PUBLIC}/" >"$SCRATCH/ex-add.out" 2>&1; then
|
||||||
if ! grep -qiE 'already|ok' "$SCRATCH/ex-add.out"; 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' ' ')"
|
blocker "wallet-exchange" "exchanges add failed — $(tail -c 160 "$SCRATCH/ex-add.out" | tr '\n' ' ')"
|
||||||
|
|
@ -883,13 +774,12 @@ e2e_one_withdraw() {
|
||||||
fi
|
fi
|
||||||
cp "$SCRATCH/accept-$tag.out" "$SCRATCH/accept.out"
|
cp "$SCRATCH/accept-$tag.out" "$SCRATCH/accept.out"
|
||||||
|
|
||||||
# Poll bank withdrawal status only (no run-until-done). force-select if stuck pending.
|
|
||||||
st=""
|
st=""
|
||||||
for i in 1 2 3 4 5 6 8 10; do
|
for i in 1 2 3 4; do
|
||||||
e2e_over && break
|
e2e_over && break
|
||||||
|
wcli run-until-done >"$SCRATCH/sel-$tag-$i.out" 2>&1 || true
|
||||||
st=$(wd_status)
|
st=$(wd_status)
|
||||||
case "$st" in selected|confirmed|aborted) break ;; esac
|
case "$st" in selected|confirmed|aborted) break ;; esac
|
||||||
sleep 0.4
|
|
||||||
done
|
done
|
||||||
if [ "$st" != "selected" ] && [ "$st" != "confirmed" ]; then
|
if [ "$st" != "selected" ] && [ "$st" != "confirmed" ]; then
|
||||||
wcli transactions >"$SCRATCH/tx-pre.json" 2>&1 || true
|
wcli transactions >"$SCRATCH/tx-pre.json" 2>&1 || true
|
||||||
|
|
@ -934,9 +824,10 @@ else:
|
||||||
fi
|
fi
|
||||||
fake_incoming_speedup
|
fake_incoming_speedup
|
||||||
|
|
||||||
# Short per-ATM settle: poll balance + bank transfer_done only (never run-until-done)
|
# Short per-ATM poll; full settle wait happens after the ladder (avoids false FAIL)
|
||||||
local ok_bal=0 r av xfer
|
local ok_bal=0 r av
|
||||||
for r in 1 2 3 4 5 6 8 10; do
|
for r in 1 2 3 4 5 6; do
|
||||||
|
wcli run-until-done >"$SCRATCH/run-$tag-$r.out" 2>&1 || true
|
||||||
wcli_bal_snap "$SCRATCH/bal-$tag.out" || wcli balance >"$SCRATCH/bal-$tag.out" 2>&1 || true
|
wcli_bal_snap "$SCRATCH/bal-$tag.out" || wcli balance >"$SCRATCH/bal-$tag.out" 2>&1 || true
|
||||||
av=$(wallet_avail_num "$SCRATCH/bal-$tag.out")
|
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
|
if python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) > 0 else 1)" "$av" 2>/dev/null; then
|
||||||
|
|
@ -945,13 +836,6 @@ else:
|
||||||
info "balance" "$(fmt_bal "$SCRATCH/bal-$tag.out")"
|
info "balance" "$(fmt_bal "$SCRATCH/bal-$tag.out")"
|
||||||
break
|
break
|
||||||
fi
|
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
|
sleep 2
|
||||||
done
|
done
|
||||||
metrics_report_coins "after-ATM-${tag}" || true
|
metrics_report_coins "after-ATM-${tag}" || true
|
||||||
|
|
@ -969,28 +853,8 @@ else:
|
||||||
return 1
|
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]
|
# One payment: e2e_one_pay AMOUNT [SUMMARY] [TAG]
|
||||||
# SUMMARY defaults to "monitoring pay $AMOUNT"; TAG defaults from amount.
|
# 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() {
|
e2e_one_pay() {
|
||||||
PAY_AMT="$1"
|
PAY_AMT="$1"
|
||||||
local PAY_SUM="${2:-monitoring pay ${PAY_AMT}}"
|
local PAY_SUM="${2:-monitoring pay ${PAY_AMT}}"
|
||||||
|
|
@ -1003,28 +867,11 @@ e2e_one_pay() {
|
||||||
section "e2e · pay $PAY_AMT · $(format_amount_alt "$PAY_AMT") · $PAY_SUM"
|
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; }
|
e2e_over && { warn "pay" "budget exhausted — skip $PAY_AMT ($PAY_SUM)"; return 1; }
|
||||||
metrics_report_coins "before-pay-${tag}" || true
|
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"
|
warn "pay $PAY_AMT" "no merchant token"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
local AUTH
|
AUTH="Authorization: Bearer secret-token:${MPW}"
|
||||||
AUTH=$(merchant_auth_header) || {
|
|
||||||
warn "pay $PAY_AMT" "no merchant token"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
# JSON-escape summary for curl -d
|
# JSON-escape summary for curl -d
|
||||||
local SUM_JSON
|
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")
|
SUM_JSON=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$PAY_SUM" 2>/dev/null || printf '"%s"' "$PAY_SUM")
|
||||||
|
|
@ -1034,9 +881,8 @@ e2e_one_pay() {
|
||||||
"${MERCHANT_PUBLIC}/instances/${INST}/private/orders" 2>"$SCRATCH/ord-$tag.err" || true
|
"${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 ! grep -q order_id "$SCRATCH/ord-$tag.json" 2>/dev/null; then
|
||||||
if [ "$E2E_REMOTE" != "1" ] && koopa_ssh_ok; then
|
if [ "$E2E_REMOTE" != "1" ] && koopa_ssh_ok; then
|
||||||
local mpw_raw="${MPW_TOKEN:-secret-token:${MPW}}"
|
|
||||||
koopa_ssh_run 20 \
|
koopa_ssh_run 20 \
|
||||||
"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'" \
|
"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'" \
|
||||||
>"$SCRATCH/ord-$tag.json" 2>"$SCRATCH/ord-$tag.err" || true
|
>"$SCRATCH/ord-$tag.json" 2>"$SCRATCH/ord-$tag.err" || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
@ -1080,37 +926,23 @@ except Exception: print("")
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
ok "wallet handle pay $PAY_AMT ($PAY_SUM)"
|
ok "wallet handle pay $PAY_AMT ($PAY_SUM)"
|
||||||
# Settle: poll merchant order + wallet transactions only (never run-until-done)
|
|
||||||
local r
|
local r
|
||||||
for r in 1 2 3 4 5 6; do
|
for r in 1 2 3; do
|
||||||
|
wcli_pay run-until-done >"$SCRATCH/pay-run-$tag.out" 2>&1 || true
|
||||||
wcli_pay transactions >"$SCRATCH/tx-$tag.out" 2>&1 || true
|
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" \
|
curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \
|
||||||
"${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
|
"${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 \
|
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 \
|
|| grep -qiE 'done|paid|success|Payment' "$SCRATCH/pay-$tag.out" 2>/dev/null \
|
||||||
|| python3 -c 'import json,sys
|
|| python3 -c 'import json,sys
|
||||||
for p in sys.argv[1:]:
|
d=json.load(open(sys.argv[1]))
|
||||||
try:
|
sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid" else 1)
|
||||||
d=json.load(open(p))
|
' "$SCRATCH/ord-paid-$tag.json" 2>/dev/null; then
|
||||||
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)"
|
ok "payment settled $PAY_AMT ($PAY_SUM · order $OID)"
|
||||||
metrics_report_coins "after-pay-${tag}" || true
|
metrics_report_coins "after-pay-${tag}" || true
|
||||||
metrics_record_flow spent "$PAY_AMT" || true
|
metrics_record_flow spent "$PAY_AMT" || true
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
sleep 1
|
|
||||||
done
|
done
|
||||||
metrics_report_coins "after-pay-fail-${tag}" || true
|
metrics_report_coins "after-pay-fail-${tag}" || true
|
||||||
warn "pay $PAY_AMT ($PAY_SUM)" "not settled for order $OID"
|
warn "pay $PAY_AMT ($PAY_SUM)" "not settled for order $OID"
|
||||||
|
|
@ -1213,34 +1045,33 @@ except Exception: print("")
|
||||||
fi
|
fi
|
||||||
ok "shop $pname" "wallet accepted pay URI"
|
ok "shop $pname" "wallet accepted pay URI"
|
||||||
|
|
||||||
# Settle: public order status + transactions only (never run-until-done)
|
|
||||||
local r AUTH=""
|
local r AUTH=""
|
||||||
AUTH=$(merchant_auth_header 2>/dev/null) || AUTH=""
|
if [ -n "${MPW:-}" ]; then
|
||||||
for r in 1 2 3 4 5 6; do
|
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
|
||||||
wcli_pay transactions >"$SCRATCH/tx-$tag.out" 2>&1 || true
|
wcli_pay transactions >"$SCRATCH/tx-$tag.out" 2>&1 || true
|
||||||
if [ -n "$AUTH" ]; then
|
if [ -n "$AUTH" ]; then
|
||||||
curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \
|
curl -skS -m 8 -o "$SCRATCH/ord-paid-$tag.json" -H "$AUTH" \
|
||||||
"${MERCHANT_PUBLIC}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
|
"${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
|
if python3 -c 'import json,sys
|
||||||
for p in sys.argv[1:]:
|
d=json.load(open(sys.argv[1]))
|
||||||
try:
|
sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="paid" else 1)
|
||||||
d=json.load(open(p))
|
' "$SCRATCH/ord-paid-$tag.json" 2>/dev/null; then
|
||||||
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)"
|
ok "shop $pname" "payment settled ($pamt · order $OID)"
|
||||||
metrics_report_coins "after-shop-${tag}" || true
|
metrics_report_coins "after-shop-${tag}" || true
|
||||||
metrics_record_flow spent "$pamt" || true
|
metrics_record_flow spent "$pamt" || true
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
sleep 1
|
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
|
||||||
done
|
done
|
||||||
metrics_report_coins "after-shop-fail-${tag}" || true
|
metrics_report_coins "after-shop-fail-${tag}" || true
|
||||||
warn "shop $pname ($pid)" "not settled for order $OID"
|
warn "shop $pname ($pid)" "not settled for order $OID"
|
||||||
|
|
@ -1248,7 +1079,6 @@ sys.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
set_group atm
|
|
||||||
section "e2e · ATM withdraw ladder"
|
section "e2e · ATM withdraw ladder"
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
WITHDRAW_OK=0
|
WITHDRAW_OK=0
|
||||||
|
|
@ -1279,14 +1109,12 @@ for WITHDRAW_AMT in $WITHDRAW_LIST; do
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
info "ATM withdraw summary" "$WITHDRAW_REPORT (ok=$WITHDRAW_OK_N lag=$WITHDRAW_LAG_N fail=$WITHDRAW_FAIL_N)"
|
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)"
|
section "e2e · load snapshot (after ATM withdraws)"
|
||||||
metrics_report_load "${METRICS_DIR}/load-after-withdraw.json" "after-withdraw" || true
|
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
|
cp -f "${METRICS_DIR}/load-after-withdraw.json" "${METRICS_DIR}/load-after.json" 2>/dev/null || true
|
||||||
metrics_report_coins "after-ATM-ladder" || true
|
metrics_report_coins "after-ATM-ladder" || true
|
||||||
|
|
||||||
# Settlement catch-up: bank may have confirmed while wallet was still empty
|
# Settlement catch-up: bank may have confirmed while wallet was still empty
|
||||||
set_group settle
|
|
||||||
section "e2e · wallet settlement (timing)"
|
section "e2e · wallet settlement (timing)"
|
||||||
if ! wait_wallet_balance 0 "${E2E_SETTLE_ROUNDS}" "${E2E_SETTLE_SLEEP}"; then
|
if ! wait_wallet_balance 0 "${E2E_SETTLE_ROUNDS}" "${E2E_SETTLE_SLEEP}"; then
|
||||||
if [ "$WITHDRAW_LAG_N" -gt 0 ] || [ "$WITHDRAW_OK_N" -gt 0 ]; then
|
if [ "$WITHDRAW_LAG_N" -gt 0 ] || [ "$WITHDRAW_OK_N" -gt 0 ]; then
|
||||||
|
|
@ -1326,7 +1154,6 @@ else
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
set_group pay
|
|
||||||
section "e2e · variable payments (if balance allows)"
|
section "e2e · variable payments (if balance allows)"
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
PAY_OK=0
|
PAY_OK=0
|
||||||
|
|
@ -1334,16 +1161,13 @@ PAY_OK_N=0
|
||||||
PAY_FAIL_N=0
|
PAY_FAIL_N=0
|
||||||
PAY_SKIP_N=0
|
PAY_SKIP_N=0
|
||||||
PAY_REPORT=""
|
PAY_REPORT=""
|
||||||
if [ -z "${MPW:-}" ] && [ "${E2E_USE_TEMPLATES:-0}" != "1" ]; then
|
if [ -z "${MPW:-}" ]; then
|
||||||
if [ "$E2E_REMOTE" = "1" ]; then
|
if [ "$E2E_REMOTE" = "1" ]; then
|
||||||
e2e_abort_auth "merchant-order" "no merchant token — set E2E_MERCHANT_TOKEN (or E2E_USE_TEMPLATES=1)"
|
e2e_abort_auth "merchant-order" "no merchant token — set E2E_MERCHANT_TOKEN"
|
||||||
fi
|
fi
|
||||||
blocker "merchant-order" "no merchant token"
|
blocker "merchant-order" "no merchant token"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
for PAY_AMT in $PAY_LIST; do
|
||||||
e2e_over && { warn "pay ladder" "time budget low — stopping more payments"; break; }
|
e2e_over && { warn "pay ladder" "time budget low — stopping more payments"; break; }
|
||||||
|
|
@ -1369,7 +1193,6 @@ for PAY_AMT in $PAY_LIST; do
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
info "pay summary" "$PAY_REPORT (ok=$PAY_OK_N fail=$PAY_FAIL_N skip=$PAY_SKIP_N)"
|
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)"
|
section "e2e · load snapshot (after payments)"
|
||||||
metrics_report_load "${METRICS_DIR}/load-after-pay.json" "after-pay" || true
|
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
|
cp -f "${METRICS_DIR}/load-after-pay.json" "${METRICS_DIR}/load-after.json" 2>/dev/null || true
|
||||||
|
|
@ -1384,47 +1207,18 @@ if [ "$PAY_OK" != "1" ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
set_group shop
|
section "e2e · GOA shop products (merchant landing catalog)"
|
||||||
section "e2e · shop products (public templates)"
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Public template POST + wallet pay — catalog random pick of N products.
|
# Public template POST + wallet pay — full catalog list, random pick of N products.
|
||||||
# GOA: goa-shop on hacktivism. TESTPAYSAN: farmer shops (jardin / fermes).
|
|
||||||
SHOP_OK=0
|
SHOP_OK=0
|
||||||
SHOP_OK_N=0
|
SHOP_OK_N=0
|
||||||
SHOP_FAIL_N=0
|
SHOP_FAIL_N=0
|
||||||
SHOP_SKIP_N=0
|
SHOP_SKIP_N=0
|
||||||
SHOP_REPORT=""
|
SHOP_REPORT=""
|
||||||
# Auto catalog for stage TESTPAYSAN when not overridden
|
if [ "$E2E_REMOTE" = "1" ] || [ "${CUR:-}" != "GOA" ]; then
|
||||||
if [ "${CUR:-}" = "TESTPAYSAN" ] && [ -z "${E2E_SHOP_PRODUCTS_SET:-}" ] && [ -z "${E2E_SHOP_FORCE_GOA:-}" ]; then
|
info "goa-shop" "SKIPPED (remote or non-GOA currency)"
|
||||||
# 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
|
else
|
||||||
# Build catalog array from E2E_SHOP_PRODUCTS (id|name|amount[|instance] lines)
|
# Build catalog array from E2E_SHOP_PRODUCTS (id|name|amount lines)
|
||||||
SHOP_CATALOG=()
|
SHOP_CATALOG=()
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
[ -z "$line" ] && continue
|
[ -z "$line" ] && continue
|
||||||
|
|
@ -1438,7 +1232,7 @@ EOF
|
||||||
case "$PICK_N" in ''|*[!0-9]*) PICK_N=2 ;; esac
|
case "$PICK_N" in ''|*[!0-9]*) PICK_N=2 ;; esac
|
||||||
if [ "$PICK_N" -lt 1 ]; then PICK_N=1; fi
|
if [ "$PICK_N" -lt 1 ]; then PICK_N=1; fi
|
||||||
if [ "$SHOP_CAT_N" -eq 0 ]; then
|
if [ "$SHOP_CAT_N" -eq 0 ]; then
|
||||||
warn "shop" "empty E2E_SHOP_PRODUCTS catalog"
|
warn "goa-shop" "empty E2E_SHOP_PRODUCTS catalog"
|
||||||
else
|
else
|
||||||
if [ "$PICK_N" -gt "$SHOP_CAT_N" ]; then PICK_N=$SHOP_CAT_N; fi
|
if [ "$PICK_N" -gt "$SHOP_CAT_N" ]; then PICK_N=$SHOP_CAT_N; fi
|
||||||
# Shuffle catalog, take first PICK_N (portable: python)
|
# Shuffle catalog, take first PICK_N (portable: python)
|
||||||
|
|
@ -1451,26 +1245,24 @@ random.shuffle(lines)
|
||||||
print('\n'.join(lines[:n]))
|
print('\n'.join(lines[:n]))
|
||||||
" "$PICK_N"
|
" "$PICK_N"
|
||||||
)
|
)
|
||||||
info "shop" "catalog ${SHOP_CAT_N} products · random pick ${PICK_N} · default instance ${E2E_SHOP_INSTANCE}"
|
info "goa-shop" "catalog ${SHOP_CAT_N} products · random pick ${PICK_N} · instance ${E2E_SHOP_INSTANCE}"
|
||||||
info "shop payto" "$E2E_SHOP_PAYTO"
|
info "goa-shop payto" "$E2E_SHOP_PAYTO"
|
||||||
info "shop pick" "$(printf '%s\n' "$SHOP_PICKED" | cut -d'|' -f1,2 | tr '\n' '; ' | sed 's/; $//')"
|
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 — pin per product if 4th field set
|
# e2e_one_pay_public_template uses global INST — temporarily pin shop instance
|
||||||
_E2E_INST_SAVE="$INST"
|
_E2E_INST_SAVE="$INST"
|
||||||
INST="${E2E_SHOP_INSTANCE}"
|
INST="${E2E_SHOP_INSTANCE}"
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
[ -z "$line" ] && continue
|
[ -z "$line" ] && continue
|
||||||
e2e_over && { warn "shop" "time budget low — stopping product pays"; break; }
|
e2e_over && { warn "goa-shop" "time budget low — stopping product pays"; break; }
|
||||||
pid=$(printf '%s' "$line" | cut -d'|' -f1)
|
pid=$(printf '%s' "$line" | cut -d'|' -f1)
|
||||||
pname=$(printf '%s' "$line" | cut -d'|' -f2)
|
pname=$(printf '%s' "$line" | cut -d'|' -f2)
|
||||||
pamt=$(printf '%s' "$line" | cut -d'|' -f3)
|
pamt=$(printf '%s' "$line" | cut -d'|' -f3)
|
||||||
pinst=$(printf '%s' "$line" | cut -d'|' -f4)
|
|
||||||
[ -z "$pid" ] || [ -z "$pamt" ] && continue
|
[ -z "$pid" ] || [ -z "$pamt" ] && continue
|
||||||
[ -z "$pname" ] && pname="$pid"
|
[ -z "$pname" ] && pname="$pid"
|
||||||
INST="${pinst:-${E2E_SHOP_INSTANCE}}"
|
|
||||||
av_now=$(wallet_avail_num)
|
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)
|
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
|
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 "shop $pname ($pid)" "skip — insufficient avail ${CUR}:${av_now} (need ${pay_n})"
|
warn "goa-shop $pname ($pid)" "skip — insufficient avail ${CUR}:${av_now} (need ${pay_n})"
|
||||||
SHOP_SKIP_N=$((SHOP_SKIP_N + 1))
|
SHOP_SKIP_N=$((SHOP_SKIP_N + 1))
|
||||||
SHOP_REPORT="${SHOP_REPORT}${SHOP_REPORT:+ }${pname}=SKIP(bal)"
|
SHOP_REPORT="${SHOP_REPORT}${SHOP_REPORT:+ }${pname}=SKIP(bal)"
|
||||||
continue
|
continue
|
||||||
|
|
@ -1494,9 +1286,9 @@ $SHOP_PICKED
|
||||||
EOF
|
EOF
|
||||||
INST="$_E2E_INST_SAVE"
|
INST="$_E2E_INST_SAVE"
|
||||||
unset _E2E_INST_SAVE
|
unset _E2E_INST_SAVE
|
||||||
info "shop summary" "$SHOP_REPORT (ok=$SHOP_OK_N fail=$SHOP_FAIL_N skip=$SHOP_SKIP_N · pick $PICK_N/$SHOP_CAT_N)"
|
info "goa-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
|
if [ "$SHOP_OK" != "1" ] && [ "$SHOP_FAIL_N" -gt 0 ]; then
|
||||||
warn "shop" "no sample product payment succeeded ($SHOP_REPORT)"
|
warn "goa-shop" "no sample product payment succeeded ($SHOP_REPORT)"
|
||||||
fi
|
fi
|
||||||
section "e2e · load snapshot (after shop pays)"
|
section "e2e · load snapshot (after shop pays)"
|
||||||
metrics_report_load "${METRICS_DIR}/load-after-shop.json" "after-shop" || true
|
metrics_report_load "${METRICS_DIR}/load-after-shop.json" "after-shop" || true
|
||||||
|
|
@ -1505,7 +1297,6 @@ EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
set_group paivana
|
|
||||||
section "e2e · paivana paywall (template GOA:4200)"
|
section "e2e · paivana paywall (template GOA:4200)"
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Public paywall: https://paivana.hacktivism.ch → 302 to merchant template "paivana".
|
# Public paywall: https://paivana.hacktivism.ch → 302 to merchant template "paivana".
|
||||||
|
|
@ -1597,12 +1388,11 @@ else
|
||||||
info "paivana summary" "${PAIVANA_REPORT:-?} · instance ${E2E_PAIVANA_INSTANCE}"
|
info "paivana summary" "${PAIVANA_REPORT:-?} · instance ${E2E_PAIVANA_INSTANCE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
set_group report
|
|
||||||
section "e2e · report"
|
section "e2e · report"
|
||||||
info "user" "$USER"
|
info "user" "$USER"
|
||||||
info "ATM withdraws" "$WITHDRAW_REPORT"
|
info "ATM withdraws" "$WITHDRAW_REPORT"
|
||||||
info "payments" "$PAY_REPORT"
|
info "payments" "$PAY_REPORT"
|
||||||
info "shop" "${SHOP_REPORT:-(n/a)}"
|
info "goa-shop" "${SHOP_REPORT:-(n/a)}"
|
||||||
info "paivana" "${PAIVANA_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 "final balance" "$(fmt_bal "$SCRATCH/bal-live.out" 2>/dev/null || echo "(n/a)")"
|
||||||
info "scratch" "$SCRATCH"
|
info "scratch" "$SCRATCH"
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,7 @@ ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||||
# shellcheck source=lib.sh
|
# shellcheck source=lib.sh
|
||||||
source "$ROOT/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_area ladder
|
||||||
set_group plan
|
|
||||||
SECTION_T0=$(date +%s)
|
SECTION_T0=$(date +%s)
|
||||||
now_ms() { python3 -c 'import time; print(int(time.time()*1000))'; }
|
now_ms() { python3 -c 'import time; print(int(time.time()*1000))'; }
|
||||||
elapsed_ms() {
|
elapsed_ms() {
|
||||||
|
|
@ -264,7 +261,6 @@ info "steps" "${LADDER_STEPS} (0 + random≥${LADDER_MIN_AMOUNT} + max-1=${CUR}:
|
||||||
info "withdraw_scale" "${LADDER_WITHDRAW_SCALE}× pay mids (fund pay ladder)"
|
info "withdraw_scale" "${LADDER_WITHDRAW_SCALE}× pay mids (fund pay ladder)"
|
||||||
info "pay_phase" "$([ "${LADDER_PAY}" = "1" ] && echo enabled || echo disabled)"
|
info "pay_phase" "$([ "${LADDER_PAY}" = "1" ] && echo enabled || echo disabled)"
|
||||||
|
|
||||||
set_group load
|
|
||||||
section "ladder · load snapshot (before withdraws)"
|
section "ladder · load snapshot (before withdraws)"
|
||||||
metrics_report_load "$LOAD_BEFORE" "ladder-start" || true
|
metrics_report_load "$LOAD_BEFORE" "ladder-start" || true
|
||||||
# Fresh wallet per rung — baseline empty (or last-rung DB if re-used later)
|
# Fresh wallet per rung — baseline empty (or last-rung DB if re-used later)
|
||||||
|
|
@ -348,7 +344,6 @@ STOP_REASON=""
|
||||||
STOP_AMOUNT=""
|
STOP_AMOUNT=""
|
||||||
declare -a RUNG_JSON=()
|
declare -a RUNG_JSON=()
|
||||||
|
|
||||||
set_group withdraw
|
|
||||||
section "ladder · phase A · withdraw (${LADDER_N} rungs)"
|
section "ladder · phase A · withdraw (${LADDER_N} rungs)"
|
||||||
rung=0
|
rung=0
|
||||||
for AMT in "$@"; do
|
for AMT in "$@"; do
|
||||||
|
|
@ -663,7 +658,12 @@ else:
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# No run-until-done (hangs / banned). Read transactions only for reserve_pub candidates.
|
# 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
|
||||||
wcli transactions >"$SCRATCH/tx-$tag.json" 2>&1 || true
|
wcli transactions >"$SCRATCH/tx-$tag.json" 2>&1 || true
|
||||||
|
|
||||||
t0=$(now_ms)
|
t0=$(now_ms)
|
||||||
|
|
@ -787,7 +787,6 @@ done
|
||||||
PAY_OK_N=0
|
PAY_OK_N=0
|
||||||
PAY_FAIL_N=0
|
PAY_FAIL_N=0
|
||||||
if [ "${LADDER_PAY}" = "1" ] && [ -n "${PAY_LIST:-}" ] && [ "$FAIL_N_L" -eq 0 ]; then
|
if [ "${LADDER_PAY}" = "1" ] && [ -n "${PAY_LIST:-}" ] && [ "$FAIL_N_L" -eq 0 ]; then
|
||||||
set_group pay
|
|
||||||
section "ladder · phase B · pay"
|
section "ladder · phase B · pay"
|
||||||
metrics_report_coins "before-pay-ladder" || true
|
metrics_report_coins "before-pay-ladder" || true
|
||||||
# Merchant secret (same as e2e)
|
# Merchant secret (same as e2e)
|
||||||
|
|
@ -960,12 +959,19 @@ except Exception: print("")
|
||||||
ms_handle=$(elapsed_ms "$t0")
|
ms_handle=$(elapsed_ms "$t0")
|
||||||
ok "handle-uri $PAMT ${ms_handle}ms"
|
ok "handle-uri $PAMT ${ms_handle}ms"
|
||||||
|
|
||||||
# Short settle polls: merchant order + wallet tx only — never run-until-done
|
# Short settle polls (bounded — no infinite hang)
|
||||||
t0=$(now_ms)
|
t0=$(now_ms)
|
||||||
settled=0
|
settled=0
|
||||||
r=0
|
r=0
|
||||||
while [ "$r" -lt "${LADDER_PAY_SETTLE_ROUNDS}" ]; do
|
while [ "$r" -lt "${LADDER_PAY_SETTLE_ROUNDS}" ]; do
|
||||||
r=$((r + 1))
|
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
|
wcli transactions >"$SCRATCH/tx-$ptag.out" 2>&1 || true
|
||||||
curl -skS -m 8 -o "$SCRATCH/ord-paid-$ptag.json" -H "$AUTH" \
|
curl -skS -m 8 -o "$SCRATCH/ord-paid-$ptag.json" -H "$AUTH" \
|
||||||
"${MER}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
|
"${MER}/instances/${INST}/private/orders/${OID}" 2>/dev/null || true
|
||||||
|
|
@ -977,7 +983,6 @@ sys.exit(0 if d.get("paid") is True or str(d.get("order_status","")).lower()=="p
|
||||||
settled=1
|
settled=1
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
sleep 0.5
|
|
||||||
done
|
done
|
||||||
ms_psettle=$(elapsed_ms "$t0")
|
ms_psettle=$(elapsed_ms "$t0")
|
||||||
ms_total=$(elapsed_ms "$t_pay")
|
ms_total=$(elapsed_ms "$t_pay")
|
||||||
|
|
@ -1019,7 +1024,6 @@ fi
|
||||||
ms_phase=$(python3 -c 'import sys,time; print(int((time.time()-float(sys.argv[1]))*1000))' "$SECTION_T0")
|
ms_phase=$(python3 -c 'import sys,time; print(int((time.time()-float(sys.argv[1]))*1000))' "$SECTION_T0")
|
||||||
|
|
||||||
# --- report ---
|
# --- report ---
|
||||||
set_group report
|
|
||||||
section "ladder · report"
|
section "ladder · report"
|
||||||
info "auto-account" "$ACCT_USER"
|
info "auto-account" "$ACCT_USER"
|
||||||
info "ok_rungs" "$OK_N"
|
info "ok_rungs" "$OK_N"
|
||||||
|
|
@ -1124,7 +1128,6 @@ PY
|
||||||
|
|
||||||
wcli balance 2>&1 | tee "$REPORT_DIR/balance-final.out" | tail -20 || true
|
wcli balance 2>&1 | tee "$REPORT_DIR/balance-final.out" | tail -20 || true
|
||||||
|
|
||||||
set_group load
|
|
||||||
section "ladder · load snapshot (after withdraws)"
|
section "ladder · load snapshot (after withdraws)"
|
||||||
metrics_report_load "$LOAD_AFTER" "ladder-end" || true
|
metrics_report_load "$LOAD_AFTER" "ladder-end" || true
|
||||||
if [ -f "$LOAD_BEFORE" ] && [ -f "$LOAD_AFTER" ]; then
|
if [ -f "$LOAD_BEFORE" ] && [ -f "$LOAD_AFTER" ]; then
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,8 @@ source "$ROOT/lib.sh"
|
||||||
# shellcheck source=metrics.sh
|
# shellcheck source=metrics.sh
|
||||||
source "$ROOT/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_area inside
|
||||||
set_group ssh
|
|
||||||
section "inside · collect from koopa"
|
section "inside · collect from koopa"
|
||||||
|
|
||||||
if [ "${SKIP_SSH}" = "1" ]; then
|
if [ "${SKIP_SSH}" = "1" ]; then
|
||||||
|
|
@ -130,20 +128,10 @@ if [ -z "$RAW" ] || ! echo "$RAW" | grep -q '^E|'; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_last_inside_grp=""
|
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
case "$line" in
|
case "$line" in
|
||||||
E\|*)
|
E\|*)
|
||||||
IFS='|' read -r _ comp level key detail <<<"$line"
|
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
|
case "$level" in
|
||||||
OK) ok "[$comp] $key${detail:+ ($detail)}" ;;
|
OK) ok "[$comp] $key${detail:+ ($detail)}" ;;
|
||||||
ERROR) err "$comp" "$key" "$detail" ;;
|
ERROR) err "$comp" "$key" "$detail" ;;
|
||||||
|
|
@ -155,7 +143,6 @@ while IFS= read -r line; do
|
||||||
done <<<"$RAW"
|
done <<<"$RAW"
|
||||||
|
|
||||||
# Host loadavg + RAM + per-container RSS/CPU (same probe as e2e/ladder)
|
# Host loadavg + RAM + per-container RSS/CPU (same probe as e2e/ladder)
|
||||||
set_group load
|
|
||||||
section "inside · load / memory"
|
section "inside · load / memory"
|
||||||
METRICS_DIR="${METRICS_DIR:-$(mktemp -d)}"
|
METRICS_DIR="${METRICS_DIR:-$(mktemp -d)}"
|
||||||
export METRICS_DIR
|
export METRICS_DIR
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,10 @@ json_currency() {
|
||||||
python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d.get("currency") or "")' "$1" 2>/dev/null || true
|
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_area sanity
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
set_group bank
|
|
||||||
section "sanity · bank"
|
section "sanity · bank"
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
expect_code "bank public /config" 200 "$BANK_PUBLIC/config"
|
expect_code "bank public /config" 200 "$BANK_PUBLIC/config"
|
||||||
|
|
@ -100,7 +98,6 @@ else
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
set_group exchange
|
|
||||||
section "sanity · exchange"
|
section "sanity · exchange"
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
expect_code "exchange public /config" 200 "$EXCHANGE_PUBLIC/config"
|
expect_code "exchange public /config" 200 "$EXCHANGE_PUBLIC/config"
|
||||||
|
|
@ -196,7 +193,6 @@ else
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
set_group merchant
|
|
||||||
section "sanity · merchant"
|
section "sanity · merchant"
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
expect_code "merchant public /config" 200 "$MERCHANT_PUBLIC/config"
|
expect_code "merchant public /config" 200 "$MERCHANT_PUBLIC/config"
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,8 @@ ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||||
# shellcheck source=lib.sh
|
# shellcheck source=lib.sh
|
||||||
source "$ROOT/lib.sh"
|
source "$ROOT/lib.sh"
|
||||||
|
|
||||||
# Area server.* — host/container ports via SSH
|
# Area server-### — host/container ports via SSH
|
||||||
set_area server
|
set_area server
|
||||||
set_group ssh
|
|
||||||
section "server · ssh ${KOOPA_SSH}"
|
section "server · ssh ${KOOPA_SSH}"
|
||||||
|
|
||||||
if ! koopa_ssh_ok; then
|
if ! koopa_ssh_ok; then
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,7 @@ ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||||
# shellcheck source=lib.sh
|
# shellcheck source=lib.sh
|
||||||
source "$ROOT/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
|
set_area www
|
||||||
section "www · public URLs · ${TALER_DOMAIN:-?} (outside-in, no SSH)"
|
section "www · public URLs · ${TALER_DOMAIN:-?} (outside-in, no SSH)"
|
||||||
|
|
||||||
|
|
@ -171,8 +169,7 @@ sys.exit(0)
|
||||||
PY
|
PY
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- exchange (core; always required) --- www.exchange-NN
|
# --- exchange (core; always required) --- www-001 …
|
||||||
set_group exchange
|
|
||||||
check_url "exchange /config" 200 "$EXCHANGE_PUBLIC/config"
|
check_url "exchange /config" 200 "$EXCHANGE_PUBLIC/config"
|
||||||
code=$(http_body "$EXCHANGE_PUBLIC/config" "$tmp/ec.json")
|
code=$(http_body "$EXCHANGE_PUBLIC/config" "$tmp/ec.json")
|
||||||
if [ "$code" = "200" ]; then
|
if [ "$code" = "200" ]; then
|
||||||
|
|
@ -222,7 +219,6 @@ fi
|
||||||
# Performance — outside-in public HTTPS latency (this runner, not loopback)
|
# Performance — outside-in public HTTPS latency (this runner, not loopback)
|
||||||
# Same spirit as bank landing-stats public probes; measured here from outside.
|
# Same spirit as bank landing-stats public probes; measured here from outside.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
set_group perf
|
|
||||||
section "www · performance · public HTTPS latency (outside-in)"
|
section "www · performance · public HTTPS latency (outside-in)"
|
||||||
|
|
||||||
# Latency thresholds (ms), outside-in from this runner.
|
# Latency thresholds (ms), outside-in from this runner.
|
||||||
|
|
@ -294,18 +290,11 @@ fi
|
||||||
info "perf note" "RTT measured from this host (outside-in)"
|
info "perf note" "RTT measured from this host (outside-in)"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Landing stats.json (public outside-in) — freshness + display fields.
|
# Landing load stats (stats.json written *inside* containers, usually public).
|
||||||
# Timestamps (required for external measurement):
|
# Prefer HTTPS /intro/stats.json (no SSH). If missing and SSH works, cat from
|
||||||
# generated_at_unix — epoch seconds (preferred)
|
# the container. Report loadavg + RSS + in-container probe ms — one line each.
|
||||||
# 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}"
|
: "${STATS_STALE_SECS:=900}" # warn if generated_at older than 15m
|
||||||
: "${STATS_FAIL_SECS:=3600}"
|
|
||||||
|
|
||||||
_fetch_landing_stats_json() {
|
_fetch_landing_stats_json() {
|
||||||
# $1=name bank|exchange|merchant $2=base URL → writes $tmp/stats-$1.json, exit 0 if ok
|
# $1=name bank|exchange|merchant $2=base URL → writes $tmp/stats-$1.json, exit 0 if ok
|
||||||
|
|
@ -333,265 +322,81 @@ _fetch_landing_stats_json() {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Validate one stats.json for monitoring display + outside age measurement.
|
report_landing_load_stats() {
|
||||||
# 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 name="$1" base="$2"
|
||||||
local f="$tmp/stats-${name}.json"
|
local f="$tmp/stats-${name}.json"
|
||||||
local line hard_missing=0
|
local line age_s
|
||||||
|
|
||||||
if ! _fetch_landing_stats_json "$name" "$base"; then
|
if ! _fetch_landing_stats_json "$name" "$base"; then
|
||||||
if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
|
if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
|
||||||
fail "stats ${name}" "HTTP/body missing · ${base}/intro/stats.json"
|
warn "perf ${name} landing-stats" "stats.json not available (public or SSH)"
|
||||||
else
|
|
||||||
warn "stats ${name}" "stats.json not available"
|
|
||||||
fi
|
fi
|
||||||
return 1
|
return
|
||||||
fi
|
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:
|
try:
|
||||||
d = json.load(open(path))
|
d = json.load(open(path))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
out("ERR", "JSON parse: %s" % str(e)[:100])
|
print("ERR parse:" + str(e)[:80])
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
if not d.get("ok", True):
|
||||||
if not isinstance(d, dict):
|
print("ERR ok=false " + str(d.get("error") or "")[:80])
|
||||||
out("ERR", "body is not a JSON object")
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
p = d.get("performance") or {}
|
||||||
if d.get("ok") is False:
|
if not isinstance(p, dict) or not p:
|
||||||
out("ERR", "ok=false · %s" % str(d.get("error") or d.get("hint") or "")[:80])
|
print("ERR no performance block")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
mem = p.get("memory") if isinstance(p.get("memory"), dict) else {}
|
||||||
# --- timestamps (outside-in age measurement) ---
|
rss = mem.get("container_rss_human") or mem.get("proc_sum_rss_human") or "?"
|
||||||
gu = d.get("generated_at_unix")
|
load = p.get("loadavg") or "?"
|
||||||
giso = d.get("generated_at") or d.get("generated_at_iso")
|
# latency fields differ by site
|
||||||
ghum = d.get("generated_at_human")
|
bits = []
|
||||||
age = None
|
for k in ("config_ms", "integration_ms", "webui_ms", "keys_ms", "terms_ms"):
|
||||||
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:
|
if k in p and p[k] is not None:
|
||||||
bits.append("%s=%s" % (k.replace("_ms", ""), p[k]))
|
bits.append("%s=%sms" % (k.replace("_ms", ""), p[k]))
|
||||||
lat = " ".join(bits) if bits else "latency=—"
|
lat = " ".join(bits) if bits else "latency=?"
|
||||||
out("OK", "performance loadavg=%s RSS=%s %s" % (load, rss, lat))
|
gen = d.get("generated_at_human") or d.get("generated_at") or "?"
|
||||||
else:
|
# staleness
|
||||||
out("OK", "performance block optional/absent")
|
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))
|
||||||
PY
|
PY
|
||||||
)
|
)
|
||||||
|
|
||||||
return 0
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
|
if [ "${CHECK_LANDING:-1}" = "1" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
|
||||||
set_group stats
|
section "www · performance · landing load stats (stats.json · in-container probes)"
|
||||||
section "www · landing stats.json (freshness + display fields · outside-in)"
|
report_landing_load_stats "bank" "$BANK_PUBLIC"
|
||||||
info "stats policy" "STALE≥${STATS_STALE_SECS}s WARN · FAIL≥${STATS_FAIL_SECS}s ERROR (0=disable fail) · need generated_at_unix"
|
report_landing_load_stats "exchange" "$EXCHANGE_PUBLIC"
|
||||||
check_landing_stats_json "bank" "$BANK_PUBLIC" || true
|
report_landing_load_stats "merchant" "$MERCHANT_PUBLIC"
|
||||||
check_landing_stats_json "exchange" "$EXCHANGE_PUBLIC" || true
|
info "perf landing-stats note" "from /intro/stats.json (public); SSH container fallback if needed"
|
||||||
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
|
fi
|
||||||
# Rollup + optional JSON for metrics_print_overall
|
# Rollup + optional JSON for metrics_print_overall
|
||||||
if [ -s "$PERF_TSV" ]; then
|
if [ -s "$PERF_TSV" ]; then
|
||||||
|
|
@ -665,8 +470,7 @@ fi
|
||||||
info "perf note" "measured from this host (outside-in); thresholds PERF_WARN_MS=${PERF_WARN_MS} PERF_FAIL_MS=${PERF_FAIL_MS}"
|
info "perf note" "measured from this host (outside-in); thresholds PERF_WARN_MS=${PERF_WARN_MS} PERF_FAIL_MS=${PERF_FAIL_MS}"
|
||||||
|
|
||||||
|
|
||||||
# Exchange terms + privacy (same www.exchange group — issues map to exchange)
|
# Terms + privacy (legal docs)
|
||||||
set_group exchange
|
|
||||||
check_legal_doc "exchange /terms" "$EXCHANGE_PUBLIC/terms" "terms|GOA|exploration|FADP|revDSG|privacy"
|
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"
|
check_legal_doc "exchange /privacy" "$EXCHANGE_PUBLIC/privacy" "privacy|FADP|revDSG|data|GOA|exploration"
|
||||||
# trailing slash: 200 or redirect to bare path
|
# trailing slash: 200 or redirect to bare path
|
||||||
|
|
@ -681,7 +485,6 @@ case "$code" in
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# --- bank ---
|
# --- bank ---
|
||||||
set_group bank
|
|
||||||
if [ "${LOCAL_STACK:-1}" = "0" ]; then
|
if [ "${LOCAL_STACK:-1}" = "0" ]; then
|
||||||
check_url_soft "bank /config" 200 "$BANK_PUBLIC/config"
|
check_url_soft "bank /config" 200 "$BANK_PUBLIC/config"
|
||||||
else
|
else
|
||||||
|
|
@ -705,7 +508,7 @@ if [ "$code" = "200" ]; then
|
||||||
check_url_soft "bank /webui/" 200 "$BANK_PUBLIC/webui/"
|
check_url_soft "bank /webui/" 200 "$BANK_PUBLIC/webui/"
|
||||||
check_url_soft "bank /intro/" 200 "$BANK_PUBLIC/intro/"
|
check_url_soft "bank /intro/" 200 "$BANK_PUBLIC/intro/"
|
||||||
check_url_soft "bank /" 302,301,200 "$BANK_PUBLIC/"
|
check_url_soft "bank /" 302,301,200 "$BANK_PUBLIC/"
|
||||||
# Auto-account: GOA shared-pool mint (hacktivism). Not used on TESTPAYSAN stage.
|
# Auto-account: credentials + shared-pool taler://withdraw (like step 2)
|
||||||
aa_code=$(http_body "$BANK_PUBLIC/intro/auto-account.json" "$tmp/aa.json")
|
aa_code=$(http_body "$BANK_PUBLIC/intro/auto-account.json" "$tmp/aa.json")
|
||||||
case "$aa_code" in
|
case "$aa_code" in
|
||||||
200)
|
200)
|
||||||
|
|
@ -715,22 +518,11 @@ if [ "$code" = "200" ]; then
|
||||||
fail "bank /intro/auto-account.json" "invalid withdraw/login ($(tr '\n' ' ' <"$tmp/aa-val" | sed 's/[[:space:]]*$//'))"
|
fail "bank /intro/auto-account.json" "invalid withdraw/login ($(tr '\n' ' ' <"$tmp/aa-val" | sed 's/[[:space:]]*$//'))"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
404|405|501)
|
405|501|404|502|503|000)
|
||||||
if [ "${EXPECT_CURRENCY:-}" = "GOA" ] || [ "${LOCAL_STACK:-0}" = "1" ]; then
|
|
||||||
fail "bank /intro/auto-account.json" "HTTP $aa_code (want 200; 405/501 = broken)"
|
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"
|
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
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
@ -753,7 +545,6 @@ else
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- merchant ---
|
# --- merchant ---
|
||||||
set_group merchant
|
|
||||||
if [ "${LOCAL_STACK:-1}" = "0" ]; then
|
if [ "${LOCAL_STACK:-1}" = "0" ]; then
|
||||||
check_url_soft "merchant /config" 200 "$MERCHANT_PUBLIC/config"
|
check_url_soft "merchant /config" 200 "$MERCHANT_PUBLIC/config"
|
||||||
else
|
else
|
||||||
|
|
@ -819,7 +610,6 @@ esac
|
||||||
# Paivana paywall (local GOA stack) — public front only; pay path is e2e
|
# Paivana paywall (local GOA stack) — public front only; pay path is e2e
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
if [ "${LOCAL_STACK:-1}" = "1" ] && [ "${E2E_PAIVANA:-1}" != "0" ]; then
|
if [ "${LOCAL_STACK:-1}" = "1" ] && [ "${E2E_PAIVANA:-1}" != "0" ]; then
|
||||||
set_group paivana
|
|
||||||
section "www · paivana paywall"
|
section "www · paivana paywall"
|
||||||
: "${PAIVANA_PUBLIC:=https://paivana.hacktivism.ch}"
|
: "${PAIVANA_PUBLIC:=https://paivana.hacktivism.ch}"
|
||||||
PAIVANA_PUBLIC="${PAIVANA_PUBLIC%/}"
|
PAIVANA_PUBLIC="${PAIVANA_PUBLIC%/}"
|
||||||
|
|
@ -856,7 +646,6 @@ if [ "${CHECK_LANDING:-1}" != "1" ]; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
set_group landing
|
|
||||||
section "www · landing exposed links · bank / merchant / exchange"
|
section "www · landing exposed links · bank / merchant / exchange"
|
||||||
|
|
||||||
# Probe one URL: print code to stdout (200 after following redirects counts as 200).
|
# Probe one URL: print code to stdout (200 after following redirects counts as 200).
|
||||||
|
|
@ -1014,24 +803,18 @@ check_one_landing() {
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Required static: qrcode.min.js. GOA og image is soft off-GOA (stage has no og-goa-shop.png).
|
# Required static assets (hard: qrcode + og; soft: qr-logo)
|
||||||
if _landing_probe "${base}/intro/qrcode.min.js"; then
|
for url in \
|
||||||
|
"${base}/intro/qrcode.min.js" \
|
||||||
|
"${base}/intro/og-goa-shop.png"
|
||||||
|
do
|
||||||
|
if _landing_probe "$url"; then
|
||||||
a_ok=$((a_ok + 1))
|
a_ok=$((a_ok + 1))
|
||||||
else
|
else
|
||||||
a_fail=$((a_fail + 1))
|
a_fail=$((a_fail + 1))
|
||||||
fail_sample="${fail_sample}${fail_sample:+; }HTTP ${_landing_code} ${base}/intro/qrcode.min.js"
|
fail_sample="${fail_sample}${fail_sample:+; }HTTP ${_landing_code} $url"
|
||||||
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
|
fi
|
||||||
|
done
|
||||||
if _landing_probe "${base}/intro/qr-logo.png"; then
|
if _landing_probe "${base}/intro/qr-logo.png"; then
|
||||||
a_ok=$((a_ok + 1))
|
a_ok=$((a_ok + 1))
|
||||||
else
|
else
|
||||||
|
|
@ -1159,15 +942,7 @@ 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:]]*$//'))"
|
fail "bank /intro/demo-withdraw.json" "invalid taler://withdraw ($(tr '\n' ' ' <"$tmp/dw-val" | sed 's/[[:space:]]*$//'))"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
404|405|501)
|
405|501|404|502|503|000)
|
||||||
# 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)"
|
fail "landing bank demo-withdraw" "HTTP $dw_code (want 200)"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||||
source "$ROOT/lib.sh"
|
source "$ROOT/lib.sh"
|
||||||
|
|
||||||
set_area versions
|
set_area versions
|
||||||
set_group outside
|
|
||||||
|
|
||||||
SUITE="${TALER_APT_SUITE:-trixie}"
|
SUITE="${TALER_APT_SUITE:-trixie}"
|
||||||
APT_BASE="${TALER_APT_BASE:-https://deb.taler.net/apt/debian}"
|
APT_BASE="${TALER_APT_BASE:-https://deb.taler.net/apt/debian}"
|
||||||
|
|
@ -167,7 +166,6 @@ fi
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 2) INSIDE — containers can reach deb.taler.net (pasta / install path)
|
# 2) INSIDE — containers can reach deb.taler.net (pasta / install path)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
set_group inside
|
|
||||||
section "versions · inside · containers → deb.taler.net"
|
section "versions · inside · containers → deb.taler.net"
|
||||||
|
|
||||||
if [ "${SKIP_SSH}" = "1" ]; then
|
if [ "${SKIP_SSH}" = "1" ]; then
|
||||||
|
|
@ -268,7 +266,6 @@ ok "collected installed packages" "${n_inst} rows"
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 3) compare installed vs trixie (and note testing)
|
# 3) compare installed vs trixie (and note testing)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
set_group compare
|
|
||||||
section "versions · compare installed vs ${SUITE}"
|
section "versions · compare installed vs ${SUITE}"
|
||||||
|
|
||||||
export BEHIND_MODE HAVE_TESTING
|
export BEHIND_MODE HAVE_TESTING
|
||||||
|
|
|
||||||
|
|
@ -193,13 +193,6 @@ apply_taler_domain() {
|
||||||
PAY_AMT="${PAY_AMT:-${EXPECT_CURRENCY}:0.01}"
|
PAY_AMT="${PAY_AMT:-${EXPECT_CURRENCY}:0.01}"
|
||||||
CREDIT_AMT="${CREDIT_AMT:-${EXPECT_CURRENCY}:100}"
|
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)
|
CHF)
|
||||||
WITHDRAW_AMT="${WITHDRAW_AMT:-CHF:20}"
|
WITHDRAW_AMT="${WITHDRAW_AMT:-CHF:20}"
|
||||||
PAY_AMT="${PAY_AMT:-CHF:0.01}"
|
PAY_AMT="${PAY_AMT:-CHF:0.01}"
|
||||||
|
|
@ -359,30 +352,17 @@ koopa_ssh_python() {
|
||||||
with_timeout "$t" ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" python3 -
|
with_timeout "$t" ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" python3 -
|
||||||
}
|
}
|
||||||
|
|
||||||
# ANSI colours — boxed badges (fg+bg) + body (label bold, detail dim).
|
# ANSI colours for tags (green OK / yellow WARN / red ERROR·BLOCKER / cyan INFO).
|
||||||
# Off: NO_COLOR=1 or CLICOLOR=0.
|
# Default: always on so pasted logs keep visible tags. Disable: NO_COLOR=1 or CLICOLOR=0.
|
||||||
if [ "${NO_COLOR:-0}" = "1" ] || [ "${CLICOLOR:-1}" = "0" ]; then
|
if [ "${NO_COLOR:-0}" = "1" ] || [ "${CLICOLOR:-1}" = "0" ]; then
|
||||||
G= R= Y= C= M= D= W= B= N=
|
G= R= Y= C= N= B=
|
||||||
BG_OK= BG_INFO= BG_WARN= BG_ERR= BG_BLK= BG_SEC=
|
|
||||||
BOX=0
|
|
||||||
else
|
else
|
||||||
G=$'\e[1;32m' # green text (totals)
|
G=$'\e[1;32m' # bold green [OK]
|
||||||
R=$'\e[1;31m' # red text
|
R=$'\e[1;31m' # bold red [ERROR] [BLOCKER]
|
||||||
Y=$'\e[1;33m' # yellow text
|
Y=$'\e[1;33m' # bold yellow [WARN]
|
||||||
C=$'\e[1;36m' # cyan text
|
C=$'\e[1;36m' # bold cyan [INFO]
|
||||||
M=$'\e[1;35m' # magenta text
|
B=$'\e[1m'
|
||||||
D=$'\e[2m' # dim tid / detail
|
N=$'\e[0m'
|
||||||
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
|
fi
|
||||||
|
|
||||||
PASS_N=0
|
PASS_N=0
|
||||||
|
|
@ -392,68 +372,17 @@ INFO_N=0
|
||||||
BLOCKERS=() # human-readable payment/withdraw blockers
|
BLOCKERS=() # human-readable payment/withdraw blockers
|
||||||
ERRORS=() # all ERROR lines (component scope)
|
ERRORS=() # all ERROR lines (component scope)
|
||||||
|
|
||||||
# Grouped test IDs: area.group-NN (e.g. www.exchange-01, e2e.pay-03)
|
# Test IDs by area: www-001, e2e-001, inside-001, …
|
||||||
# set_area www # phase: www | e2e | inside | versions | …
|
# Usage: set_area www then each ok/fail/warn/info/blocker/err auto-numbers.
|
||||||
# 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_AREA=""
|
||||||
TEST_GROUP=""
|
|
||||||
TEST_N=0
|
TEST_N=0
|
||||||
|
# Last issued id (www-001); set by _take_tid — not via $(…) so TEST_N persists.
|
||||||
LAST_TID=""
|
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() {
|
set_area() {
|
||||||
# leave previous group counter saved
|
|
||||||
_tid_save
|
|
||||||
TEST_AREA="$1"
|
TEST_AREA="$1"
|
||||||
TEST_GROUP=""
|
|
||||||
TEST_N=0
|
TEST_N=0
|
||||||
LAST_TID=""
|
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).
|
# Assign next id into LAST_TID (must not run in a subshell).
|
||||||
_take_tid() {
|
_take_tid() {
|
||||||
LAST_TID=""
|
LAST_TID=""
|
||||||
|
|
@ -461,69 +390,49 @@ _take_tid() {
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
TEST_N=$((TEST_N + 1))
|
TEST_N=$((TEST_N + 1))
|
||||||
if [ -n "${TEST_GROUP:-}" ]; then
|
LAST_TID=$(printf '%s-%03d' "$TEST_AREA" "$TEST_N")
|
||||||
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() {
|
_fmt_tid() {
|
||||||
|
# prefix "www-001 " or empty
|
||||||
if [ -n "${LAST_TID:-}" ]; then
|
if [ -n "${LAST_TID:-}" ]; then
|
||||||
printf '%s%s%s ' "$D" "$LAST_TID" "$N"
|
printf '%s ' "$LAST_TID"
|
||||||
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
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
ok() {
|
ok() {
|
||||||
# ok "what is good" ["concrete evidence: HTTP 200 · 12ms · …"]
|
# Forms (same idea as info/warn):
|
||||||
|
# ok "what passed"
|
||||||
|
# ok "what passed" "detail / ms / bytes / …"
|
||||||
local label="$1" detail="${2:-}"
|
local label="$1" detail="${2:-}"
|
||||||
_take_tid
|
_take_tid
|
||||||
_msg_line "$BG_OK" "OK" "$W" "$label" "$detail"
|
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
|
||||||
PASS_N=$((PASS_N + 1))
|
PASS_N=$((PASS_N + 1))
|
||||||
}
|
}
|
||||||
# component-scoped error: err bank "what failed" "why / HTTP / path"
|
# component-scoped error: err bank "libeufin down" "detail"
|
||||||
err() {
|
err() {
|
||||||
local comp="$1" msg="$2" detail="${3:-}"
|
local comp="$1" msg="$2" detail="${3:-}"
|
||||||
_take_tid
|
_take_tid
|
||||||
_msg_line "$BG_ERR" "ERROR" "$W" "${comp}: ${msg}" "$detail"
|
printf '%s[ERROR]%s %s%s: %s%s\n' "$R" "$N" "$(_fmt_tid)" "$comp" "$msg" "${detail:+ — $detail}"
|
||||||
FAIL_N=$((FAIL_N + 1))
|
FAIL_N=$((FAIL_N + 1))
|
||||||
ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ · $detail}")
|
ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ — $detail}")
|
||||||
}
|
}
|
||||||
# fail "what failed" ["why / HTTP code / path"]
|
# legacy fail label ...
|
||||||
fail() {
|
fail() {
|
||||||
local label="$1" detail="${2:-}"
|
local label="$1" detail="${2:-}"
|
||||||
_take_tid
|
_take_tid
|
||||||
_msg_line "$BG_ERR" "ERROR" "$W" "$label" "$detail"
|
printf '%s[ERROR]%s %s%s%s\n' "$R" "$N" "$(_fmt_tid)" "$label" "${detail:+ — $detail}"
|
||||||
FAIL_N=$((FAIL_N + 1))
|
FAIL_N=$((FAIL_N + 1))
|
||||||
ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ · $detail}")
|
ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ — $detail}")
|
||||||
}
|
}
|
||||||
warn() {
|
warn() {
|
||||||
# warn "what is soft-bad" ["why still ok to continue"]
|
# Forms (same idea as err; always try to state the problem):
|
||||||
# warn component "what" "why"
|
# warn "problem"
|
||||||
|
# warn "problem" "why / context"
|
||||||
|
# warn component "problem" "why / context"
|
||||||
local a1="${1:-}" a2="${2:-}" a3="${3:-}"
|
local a1="${1:-}" a2="${2:-}" a3="${3:-}"
|
||||||
local head detail
|
local head detail
|
||||||
_take_tid
|
_take_tid
|
||||||
|
|
@ -537,78 +446,56 @@ warn() {
|
||||||
head="$a1"
|
head="$a1"
|
||||||
detail=""
|
detail=""
|
||||||
fi
|
fi
|
||||||
_msg_line "$BG_WARN" "WARN" "$W" "$head" "$detail"
|
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
|
||||||
WARN_N=$((WARN_N + 1))
|
WARN_N=$((WARN_N + 1))
|
||||||
}
|
}
|
||||||
info() {
|
info() {
|
||||||
# info "topic" ["concrete fact / value / next step"]
|
|
||||||
local label="$1" detail="${2:-}"
|
local label="$1" detail="${2:-}"
|
||||||
_take_tid
|
_take_tid
|
||||||
_msg_line "$BG_INFO" "INFO" "$W" "$label" "$detail"
|
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
|
||||||
INFO_N=$((INFO_N + 1))
|
INFO_N=$((INFO_N + 1))
|
||||||
}
|
}
|
||||||
blocker() {
|
blocker() {
|
||||||
# Hard stop on pay/withdraw path: blocker "step" "why it cannot continue"
|
# Payment/withdraw path cannot proceed because of this
|
||||||
local step="$1" msg="$2"
|
local step="$1" msg="$2"
|
||||||
_take_tid
|
_take_tid
|
||||||
_msg_line "$BG_BLK" "BLOCKER" "$W" "${step}" "$msg"
|
printf '%s[BLOCKER]%s %s%s: %s\n' "$R$B" "$N" "$(_fmt_tid)" "$step" "$msg"
|
||||||
BLOCKERS+=("${LAST_TID:+$LAST_TID }[$step] $msg")
|
BLOCKERS+=("${LAST_TID:+$LAST_TID }[$step] $msg")
|
||||||
FAIL_N=$((FAIL_N + 1))
|
FAIL_N=$((FAIL_N + 1))
|
||||||
ERRORS+=("BLOCKER ${LAST_TID:+$LAST_TID }[$step] $msg")
|
ERRORS+=("BLOCKER ${LAST_TID:+$LAST_TID }[$step] $msg")
|
||||||
}
|
}
|
||||||
section() {
|
section() { printf '\n%s== %s ==%s\n' "$B" "$*" "$N"; }
|
||||||
# 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() {
|
summary() {
|
||||||
echo ""
|
echo ""
|
||||||
if [ "${#BLOCKERS[@]}" -gt 0 ]; then
|
if [ "${#BLOCKERS[@]}" -gt 0 ]; then
|
||||||
if [ "${BOX:-0}" = "1" ]; then
|
printf '%s--- BLOCKERS (fix/withdraw path) ---%s\n' "$R$B" "$N"
|
||||||
printf -- '%s┌ BLOCKERS · pay/withdraw cannot finish ┐%s\n' "$BG_BLK" "$N"
|
|
||||||
else
|
|
||||||
printf -- '--- BLOCKERS (pay/withdraw cannot finish) ---\n'
|
|
||||||
fi
|
|
||||||
local b
|
local b
|
||||||
for b in "${BLOCKERS[@]}"; do
|
for b in "${BLOCKERS[@]}"; do
|
||||||
printf -- '%s •%s %s%s%s\n' "$M" "$N" "$W" "$b" "$N"
|
printf '%s • %s%s\n' "$R" "$b" "$N"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
if [ "${#ERRORS[@]}" -gt 0 ] && [ "${#BLOCKERS[@]}" -lt "${#ERRORS[@]}" ]; then
|
if [ "${#ERRORS[@]}" -gt 0 ] && [ "${#BLOCKERS[@]}" -lt "${#ERRORS[@]}" ]; then
|
||||||
if [ "${BOX:-0}" = "1" ]; then
|
printf '%s--- ERRORS ---%s\n' "$R" "$N"
|
||||||
printf -- '%s┌ ERRORS · failed checks ┐%s\n' "$BG_ERR" "$N"
|
|
||||||
else
|
|
||||||
printf -- '--- ERRORS (failed checks) ---\n'
|
|
||||||
fi
|
|
||||||
local e
|
local e
|
||||||
for e in "${ERRORS[@]}"; do
|
for e in "${ERRORS[@]}"; do
|
||||||
case "$e" in BLOCKER*) continue ;; esac
|
case "$e" in BLOCKER*) continue ;; esac
|
||||||
printf -- '%s •%s %s%s%s\n' "$R" "$N" "$W" "$e" "$N"
|
printf '%s • %s%s\n' "$R" "$e" "$N"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
printf -- '%stotals:%s %s%d OK%s' "$D" "$N" "$G" "$PASS_N" "$N"
|
printf 'totals: %s%d OK%s' "$G" "$PASS_N" "$N"
|
||||||
[ "$FAIL_N" -gt 0 ] && printf -- ', %s%d ERROR%s' "$R" "$FAIL_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"
|
[ "$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"
|
[ "$INFO_N" -gt 0 ] && printf ', %d INFO' "$INFO_N"
|
||||||
[ "${#BLOCKERS[@]}" -gt 0 ] && printf -- ', %s%d BLOCKER%s' "$M" "${#BLOCKERS[@]}" "$N"
|
[ "${#BLOCKERS[@]}" -gt 0 ] && printf ', %s%d BLOCKER%s' "$R" "${#BLOCKERS[@]}" "$N"
|
||||||
printf '\n'
|
printf '\n'
|
||||||
[ "$FAIL_N" -eq 0 ]
|
[ "$FAIL_N" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue