docs: new root as prior history lost (orphan + GC); ~215 commits not recoverable
This commit is contained in:
commit
96961f23f5
268 changed files with 24161 additions and 0 deletions
100
scripts/taler-monitoring/README.md
Normal file
100
scripts/taler-monitoring/README.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# taler-monitoring
|
||||
|
||||
Report for the **GOA** stack with clear severity tags and **per-area test IDs**:
|
||||
|
||||
| Tag | Meaning |
|
||||
|-----|---------|
|
||||
| `[OK]` | check passed |
|
||||
| `[INFO]` | inside status (container, ports, log noise) |
|
||||
| `[WARN]` | degraded but maybe not fatal |
|
||||
| `[ERROR]` | component problem |
|
||||
| `[BLOCKER]` | **withdraw/pay path cannot complete** because of this |
|
||||
|
||||
IDs: **`www-001`**, **`inside-001`**, **`versions-001`**, **`sanity-001`**, **`server-001`**, **`e2e-001`** …
|
||||
Catalog: **[TESTS.md](./TESTS.md)**.
|
||||
|
||||
```text
|
||||
[OK] www-001 exchange /config https://exchange…/config
|
||||
[BLOCKER] e2e-015 prereq: merchant HTTP 502
|
||||
```
|
||||
|
||||
End of each phase: totals + list of **BLOCKERS** and **ERRORS**.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Local GOA stack (may use SSH for inside/e2e)
|
||||
./taler-monitoring.sh # urls + inside + versions + e2e
|
||||
./taler-monitoring.sh inside # containers on koopa
|
||||
./taler-monitoring.sh versions # deb.taler.net + package versions vs trixie
|
||||
./taler-monitoring.sh sanity
|
||||
./taler-monitoring.sh e2e
|
||||
|
||||
# Other domains — public HTTPS only, never SSH
|
||||
./taler-monitoring.sh -d taler.net
|
||||
./taler-monitoring.sh --domain taler-ops.ch
|
||||
./taler-monitoring.sh -d demo.taler.net urls
|
||||
./taler-monitoring.sh taler.net # bare domain = same as -d
|
||||
```
|
||||
|
||||
| Domain | Bank | Exchange | Merchant | Currency |
|
||||
|--------|------|----------|----------|----------|
|
||||
| `hacktivism.ch` (default) | bank.hacktivism.ch | exchange.hacktivism.ch | taler.hacktivism.ch | GOA |
|
||||
| `taler.net` / `demo.taler.net` | bank.demo.taler.net | exchange.demo.taler.net | backend.demo.taler.net | KUDOS |
|
||||
| `taler-ops.ch` | bank.* (probe) | exchange.taler-ops.ch | backend.* (probe) | CHF |
|
||||
| other | bank.DOMAIN | exchange.DOMAIN | backend/taler/merchant (probe) | any |
|
||||
|
||||
**SSH only for koopa** (`hacktivism.ch` / `-d koopa`).
|
||||
|
||||
Other domains: never SSH. Optional **e2e** aborts cleanly on login/KYC.
|
||||
|
||||
### E2E amounts (variable)
|
||||
|
||||
| | Local (koopa) | Remote |
|
||||
|--|---------------|--------|
|
||||
| **ATM withdraw** | 20 · 50 · 100 · 200 | 10 · 20 · 50 |
|
||||
| **Pay ladder** | 0.01 … 10 | 0.01 … 1 |
|
||||
|
||||
```bash
|
||||
./taler-monitoring.sh e2e
|
||||
./taler-monitoring.sh -d taler.net urls e2e
|
||||
# customize:
|
||||
E2E_WITHDRAW_VALUES="20 50 100" E2E_PAY_VALUES="0.05 1 5" ./taler-monitoring.sh e2e
|
||||
E2E_VARIABLE=0 WITHDRAW_AMT=GOA:50 PAY_AMT=GOA:1 ./taler-monitoring.sh e2e # single fixed
|
||||
# GOA shop catalog (local hacktivism): full list in E2E_SHOP_PRODUCTS; each run
|
||||
# shuffles and pays E2E_SHOP_PICK_N products (default 2).
|
||||
# E2E_SHOP_PRODUCTS lines: id|Product name|GOA:amount
|
||||
# E2E_SHOP_PICK_N=2
|
||||
# (landing QR = taler://pay-template/…/{id}; popup = live taler://pay after POST templates/{id})
|
||||
# remote secrets:
|
||||
# E2E_BANK_ADMIN_PASS=… E2E_MERCHANT_TOKEN=…
|
||||
```
|
||||
|
||||
## Phases
|
||||
|
||||
| Phase | What |
|
||||
|-------|------|
|
||||
| **inside** | SSH koopa: processes, postgres, local /config,/keys, wirewatch, recent log ERRORs |
|
||||
| **versions** | `deb.taler.net` reachable (InRelease/Packages/pool `.deb`); containers can reach it + have apt source; installed Taler packages vs **trixie** |
|
||||
| **sanity** | bank · exchange · merchant sections (public + server) |
|
||||
| **e2e** | account → credit → withdraw → wallet → confirm → order → pay |
|
||||
|
||||
```bash
|
||||
# package suite (default trixie on deb.taler.net)
|
||||
TALER_APT_SUITE=trixie ./taler-monitoring.sh versions
|
||||
TALER_PKG_BEHIND=error ./taler-monitoring.sh versions # any behind = ERROR
|
||||
```
|
||||
|
||||
E2E maps failures to blockers, e.g.:
|
||||
|
||||
- `bank-confirm HTTP 409` → wallet did not select exchange
|
||||
- `no GOA balance` → wirewatch / transfer
|
||||
- `create order failed` → merchant auth/instance
|
||||
- `Alarm clock` during pay → usually missing `handle-uri --yes` or wrong pay URI (must be `…/instances/{inst}/{oid}/?c={token}` from merchant `taler_pay_uri`)
|
||||
- `insufficient balance` → withdraw incomplete
|
||||
|
||||
## Needs
|
||||
|
||||
- SSH `koopa` (inside/sanity server bits)
|
||||
- secrets under `koopa-admin-secrets/...` for e2e
|
||||
- `taler-wallet-cli` for e2e
|
||||
165
scripts/taler-monitoring/TESTS.md
Normal file
165
scripts/taler-monitoring/TESTS.md
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
# taler-monitoring — test IDs by area
|
||||
|
||||
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
|
||||
[OK] www-001 exchange /config https://exchange…/config
|
||||
[ERROR] e2e-012 bank-auth: admin token failed
|
||||
[BLOCKER] e2e-015 prereq: merchant HTTP 502
|
||||
```
|
||||
|
||||
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 — public URLs (`./taler-monitoring.sh urls`)
|
||||
|
||||
| ID | Check |
|
||||
|----|--------|
|
||||
| www-… | exchange `/config`, currency, **alt_unit_names** |
|
||||
| www-… | exchange `/keys` (+ alt_unit_names soft) |
|
||||
| www-… | exchange `/intro/`, `/` (302→intro) |
|
||||
| www-… | **exchange `/terms`** body (not empty / not API error) |
|
||||
| www-… | **exchange `/privacy`** body |
|
||||
| www-… | exchange `/terms/` (200 or redirect) |
|
||||
| www-… | bank `/config`, currency, **alt_unit_names** |
|
||||
| www-… | bank integration / webui / intro / `/` |
|
||||
| www-… | **bank `/terms`** body |
|
||||
| www-… | **bank `/privacy`** (or `/intro/privacy.html` fallback) |
|
||||
| www-… | merchant `/config`, currency, currencies alt_unit_names |
|
||||
| www-… | each merchant `exchanges[]` `/config` alt_unit_names |
|
||||
| www-… | merchant `/intro/`, `/webui/`, `/` |
|
||||
| www-… | **merchant `/terms`** body (dual-currency notice) |
|
||||
| www-… | **merchant `/privacy`** body (must not be `not configured`) |
|
||||
| www-… | merchant `/terms/` redirect |
|
||||
| www-… | **landing 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:PORT/taler-integration/…` + integration op HTTP 200 |
|
||||
| www-… | bank `/intro/auto-account.json` (earlier) → same withdraw shape, **no payto_uri**, login at `/webui/` |
|
||||
| www-… | **performance** (outside-in): public HTTPS RTT for bank `/config`, `/taler-integration/config`, `/webui/`, `/intro/`, `stats.json`; exchange `/config`, `/keys`, `/intro/`; merchant `/config`, `/webui/`, `/intro/` — report ms; WARN ≥ `PERF_WARN_MS` (default 8000); **ERROR ≥ `PERF_FAIL_MS` (default 20000)** |
|
||||
|
||||
**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 URLs via Caddy), not container loopback. HTTP must match expect (usually 200); latency is reported on the OK line. Slow ≥ `PERF_WARN_MS` → WARN only (no ERROR on slowness alone).
|
||||
|
||||
**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/…:port/taler-integration/…`, never payto.
|
||||
|
||||
**alt_unit_names rule:** wallet codec requires a non-empty map including scale key `"0"`. For multi-currency merchant, also follow every entry in `exchanges[]` and check that exchange’s public `/config`.
|
||||
|
||||
(IDs after a failed early check may shift if later soft checks are skipped when body missing — numbering follows **executed** checks.)
|
||||
|
||||
---
|
||||
|
||||
## inside — koopa SSH (`./taler-monitoring.sh inside`)
|
||||
|
||||
| ID | Check (typical order) |
|
||||
|----|------------------------|
|
||||
| inside-001 | ssh koopa |
|
||||
| inside-002+ | per-component emit: container, ports, libeufin/httpd, postgres, local `/config`/`/keys`, wirewatch, DNS pin, caddy |
|
||||
|
||||
Remote lines `E|comp|LEVEL|key|detail` each become one numbered result.
|
||||
|
||||
---
|
||||
|
||||
## sanity — bank · exchange · merchant (`./taler-monitoring.sh sanity`)
|
||||
|
||||
| ID | Section |
|
||||
|----|---------|
|
||||
| sanity-001… | bank public + server |
|
||||
| sanity-… | exchange public + server |
|
||||
| sanity-… | merchant public + server |
|
||||
|
||||
Sequential through the whole script (one `set_area sanity`).
|
||||
|
||||
---
|
||||
|
||||
## versions — packages vs deb.taler.net (`./taler-monitoring.sh versions`)
|
||||
|
||||
### Outside (runner / public network — no SSH)
|
||||
|
||||
| ID (order) | Check |
|
||||
|------------|--------|
|
||||
| versions-… | DNS `deb.taler.net` |
|
||||
| versions-… | HTTPS portal + apt base URL |
|
||||
| versions-… | suite `InRelease` / `Release` |
|
||||
| versions-… | suite `Packages` + `Packages.gz` |
|
||||
| versions-… | sample pool `.deb` fetchable (Range 200/206) |
|
||||
| versions-… | suite offers `taler-exchange`, `taler-merchant`, `libeufin-bank` |
|
||||
| versions-… | optional `trixie-testing` Packages |
|
||||
| versions-… | TLS verify (soft) |
|
||||
|
||||
### Inside (SSH koopa containers)
|
||||
|
||||
| ID | Check |
|
||||
|----|--------|
|
||||
| versions-… | ssh koopa |
|
||||
| versions-… | each container → `InRelease` (pasta can reach apt repo) |
|
||||
| versions-… | each container lists `deb.taler.net` in apt sources |
|
||||
| versions-… | each installed `taler*` / `libeufin*` / `libtaler*` / `libdonau*` vs suite version |
|
||||
| versions-… | core packages installed (`taler-exchange`, `libeufin-bank`, `taler-merchant`) |
|
||||
|
||||
Outside always runs. Inside skipped with `SKIP_SSH=1` (still reports outside results).
|
||||
|
||||
Compare rules:
|
||||
|
||||
- **match** suite → OK
|
||||
- **ahead** of suite (often testing/dev) → INFO
|
||||
- **behind** suite → ERROR for core packages, WARN otherwise (`TALER_PKG_BEHIND=error` forces ERROR)
|
||||
|
||||
Default suite: **trixie** (`TALER_APT_SUITE`, `TALER_APT_BASE=https://deb.taler.net/apt/debian`).
|
||||
|
||||
Without SSH (`SKIP_SSH=1` or remote domain): still runs outside-in repo checks; skips container install compare.
|
||||
|
||||
---
|
||||
|
||||
## 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`)
|
||||
|
||||
| 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-… | **GOA shop products** — full catalog list; **random pick of 2** (override `E2E_SHOP_PICK_N`) |
|
||||
| e2e-… | balances, dig on failure |
|
||||
|
||||
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`.
|
||||
|
||||
---
|
||||
|
||||
## 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 -d taler.net urls
|
||||
```
|
||||
1220
scripts/taler-monitoring/check_e2e.sh
Executable file
1220
scripts/taler-monitoring/check_e2e.sh
Executable file
File diff suppressed because it is too large
Load diff
141
scripts/taler-monitoring/check_inside.sh
Executable file
141
scripts/taler-monitoring/check_inside.sh
Executable file
|
|
@ -0,0 +1,141 @@
|
|||
#!/usr/bin/env bash
|
||||
# Inside status for bank / exchange / merchant. Hard-capped SSH — never hang forever.
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
# Area inside-### — container / process state on koopa (SSH)
|
||||
set_area inside
|
||||
section "inside · collect from koopa"
|
||||
|
||||
if [ "${SKIP_SSH}" = "1" ]; then
|
||||
warn "ssh" "SKIP_SSH=1 — skipped"
|
||||
summary
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! koopa_ssh_ok; then
|
||||
err "ssh" "cannot reach ${KOOPA_SSH} in ${SSH_CONNECT_TIMEOUT}s — set SKIP_SSH=1 to skip inside"
|
||||
summary
|
||||
exit 1
|
||||
fi
|
||||
ok "ssh ${KOOPA_SSH}"
|
||||
|
||||
# One short remote script (≤ SSH_CMD_TIMEOUT). Every slow step is local curl -m 3 or quick pgrep.
|
||||
RAW=$(
|
||||
koopa_ssh_bash "${SSH_CMD_TIMEOUT}" <<'REMOTE' || true
|
||||
set +e
|
||||
emit() { printf 'E|%s|%s|%s|%s\n' "$1" "$2" "$3" "$(printf '%s' "${4:-}" | tr '\n\r' ' ' | head -c 200)"; }
|
||||
# quick curl
|
||||
hc() { curl -skS -m 3 -o /tmp/mb -w '%{http_code}' "$1" 2>/dev/null || echo 000; }
|
||||
# quick process check inside container (pgrep only)
|
||||
hasp() { podman exec "$1" pgrep -f "$2" >/dev/null 2>&1; }
|
||||
|
||||
BANK=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'hacktivism-bank|taler-bank' | head -1)
|
||||
[ -z "$BANK" ] && BANK=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -i bank | head -1)
|
||||
EX=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -i exchange | head -1)
|
||||
MER=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -E '^taler-hacktivism$' | head -1)
|
||||
[ -z "$MER" ] && MER=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'merchant|hacktivism' | grep -viE 'bank|exchange' | head -1)
|
||||
|
||||
# Domain resolve inside container (wirewatch needs bank.hacktivism.ch → real IP)
|
||||
# emit: comp LEVEL dns "host → ip" or fail
|
||||
check_dns() {
|
||||
local comp="$1" ctr="$2" host="$3"
|
||||
local line ip code
|
||||
# Prefer IPv4 (wirewatch/libcurl often happier; avoid dead AAAA)
|
||||
line=$(podman exec "$ctr" getent ahostsv4 "$host" 2>/dev/null | head -1)
|
||||
[ -z "$line" ] && line=$(podman exec "$ctr" getent hosts "$host" 2>/dev/null | head -1)
|
||||
ip=$(echo "$line" | awk '{print $1}')
|
||||
if [ -z "$ip" ]; then
|
||||
emit "$comp" ERROR "dns $host" "no resolve — run pin-container-hosts.sh"
|
||||
return 1
|
||||
fi
|
||||
# 127.0.0.1 is almost always wrong for public bank/exchange from inside pasta
|
||||
if [ "$ip" = "127.0.0.1" ] || [ "$ip" = "::1" ]; then
|
||||
emit "$comp" ERROR "dns $host" "$ip (loopback — wirewatch will fail)"
|
||||
return 1
|
||||
fi
|
||||
code=$(podman exec "$ctr" curl -skS -m 3 -o /dev/null -w '%{http_code}' "https://${host}/config" 2>/dev/null || echo 000)
|
||||
if [ "$code" = "200" ]; then
|
||||
emit "$comp" OK "dns $host" "→ $ip /config=$code"
|
||||
else
|
||||
emit "$comp" WARN "dns $host" "→ $ip /config=$code"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -z "$BANK" ]; then emit bank ERROR container "not running"
|
||||
else
|
||||
emit bank INFO container "$(podman ps --filter name=$BANK --format '{{.Names}} {{.Status}}' | head -1)"
|
||||
emit bank INFO ports "$(podman ps --filter name=$BANK --format '{{.Ports}}' | head -1)"
|
||||
hasp "$BANK" 'MainKt serve|libeufin-bank serve' && emit bank OK libeufin "running" || emit bank ERROR libeufin "not running — API/withdraw dead"
|
||||
podman exec "$BANK" pg_isready -q 2>/dev/null && emit bank OK postgres "ready" || emit bank ERROR postgres "not ready"
|
||||
c=$(hc http://127.0.0.1:9012/config)
|
||||
[ "$c" = "200" ] && emit bank OK "local /config" "HTTP $c" || emit bank ERROR "local /config" "HTTP $c"
|
||||
c=$(hc http://127.0.0.1:9012/taler-integration/config)
|
||||
[ "$c" = "200" ] && emit bank OK "local integration" "HTTP $c" || emit bank ERROR "local integration" "HTTP $c"
|
||||
hasp "$BANK" 'nginx' && emit bank OK nginx ":9013" || emit bank WARN nginx "not running"
|
||||
check_dns bank "$BANK" bank.hacktivism.ch || true
|
||||
check_dns bank "$BANK" exchange.hacktivism.ch || true
|
||||
fi
|
||||
|
||||
if [ -z "$EX" ]; then emit exchange ERROR container "not running"
|
||||
else
|
||||
emit exchange INFO container "$(podman ps --filter name=$EX --format '{{.Names}} {{.Status}}' | head -1)"
|
||||
c=$(hc http://127.0.0.1:9011/config)
|
||||
[ "$c" = "200" ] && emit exchange OK "local /config" "HTTP $c" || emit exchange ERROR "local /config" "HTTP $c"
|
||||
c=$(curl -sS -m 5 -o /dev/null -w '%{http_code}' http://127.0.0.1:9011/keys 2>/dev/null || echo 000)
|
||||
[ "$c" = "200" ] && emit exchange OK "local /keys" "HTTP $c" || emit exchange ERROR "local /keys" "HTTP $c"
|
||||
hasp "$EX" 'taler-exchange-httpd' && emit exchange OK httpd "running" || emit exchange ERROR httpd "not running"
|
||||
hasp "$EX" 'taler-exchange-wirewatch' && emit exchange OK wirewatch "running" || emit exchange ERROR wirewatch "not running — withdraw stuck after bank confirm"
|
||||
hasp "$EX" 'taler-exchange-aggregator' && emit exchange OK aggregator "running" || emit exchange WARN aggregator "not running"
|
||||
hasp "$EX" 'taler-exchange-transfer' && emit exchange OK transfer "running" || emit exchange WARN transfer "not running"
|
||||
# critical for wire gateway
|
||||
check_dns exchange "$EX" bank.hacktivism.ch || true
|
||||
check_dns exchange "$EX" exchange.hacktivism.ch || true
|
||||
check_dns exchange "$EX" taler.hacktivism.ch || true
|
||||
fi
|
||||
|
||||
if [ -z "$MER" ]; then emit merchant ERROR container "not running"
|
||||
else
|
||||
emit merchant INFO container "$(podman ps --filter name=$MER --format '{{.Names}} {{.Status}}' | head -1)"
|
||||
c=$(hc https://127.0.0.1:9010/config)
|
||||
[ "$c" = "200" ] && emit merchant OK "local /config" "HTTP $c" || emit merchant ERROR "local /config" "HTTP $c"
|
||||
hasp "$MER" 'taler-merchant-httpd' && emit merchant OK httpd "running" || emit merchant ERROR httpd "not running"
|
||||
hasp "$MER" 'taler-merchant-wirewatch' && emit merchant OK wirewatch "running" || emit merchant WARN wirewatch "not running"
|
||||
hasp "$MER" 'taler-merchant-depositcheck' && emit merchant OK depositcheck "running" || emit merchant WARN depositcheck "not running"
|
||||
check_dns merchant "$MER" bank.hacktivism.ch || true
|
||||
check_dns merchant "$MER" exchange.hacktivism.ch || true
|
||||
check_dns merchant "$MER" taler.hacktivism.ch || true
|
||||
fi
|
||||
|
||||
if systemctl is-active caddy >/dev/null 2>&1 || pgrep -x caddy >/dev/null 2>&1; then
|
||||
emit caddy OK process "active"
|
||||
else
|
||||
emit caddy ERROR process "not active"
|
||||
fi
|
||||
echo DONE
|
||||
REMOTE
|
||||
)
|
||||
|
||||
if [ -z "$RAW" ] || ! echo "$RAW" | grep -q '^E|'; then
|
||||
err "ssh" "remote timed out or empty (cap ${SSH_CMD_TIMEOUT}s)"
|
||||
summary
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
E\|*)
|
||||
IFS='|' read -r _ comp level key detail <<<"$line"
|
||||
case "$level" in
|
||||
OK) ok "[$comp] $key${detail:+ ($detail)}" ;;
|
||||
ERROR) err "$comp" "$key" "$detail" ;;
|
||||
WARN) warn "[$comp] $key" "$detail" ;;
|
||||
INFO) info "[$comp] $key" "$detail" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
done <<<"$RAW"
|
||||
|
||||
summary
|
||||
281
scripts/taler-monitoring/check_sanity.sh
Executable file
281
scripts/taler-monitoring/check_sanity.sh
Executable file
|
|
@ -0,0 +1,281 @@
|
|||
#!/usr/bin/env bash
|
||||
# Sanity checks for bank · exchange · merchant (public + server-side).
|
||||
# Sections are independent; continues after failures.
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
expect_code() {
|
||||
local label="$1" want="$2" url="$3"
|
||||
local code
|
||||
code=$(http_code "$url")
|
||||
case ",$want," in
|
||||
*",$code,"*) ok "$label" ;;
|
||||
*) fail "$label" "HTTP $code (want $want) $url" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
json_currency() {
|
||||
python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d.get("currency") or "")' "$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Area sanity-### — public + optional server-side per component
|
||||
set_area sanity
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "sanity · bank"
|
||||
# ---------------------------------------------------------------------------
|
||||
expect_code "bank public /config" 200 "$BANK_PUBLIC/config"
|
||||
expect_code "bank public /taler-integration/config" 200 "$BANK_PUBLIC/taler-integration/config"
|
||||
expect_code "bank public /webui/" 200 "$BANK_PUBLIC/webui/"
|
||||
|
||||
code=$(http_body "$BANK_PUBLIC/config" "$tmp/bank-config.json")
|
||||
if [ "$code" = "200" ]; then
|
||||
cur=$(json_currency "$tmp/bank-config.json")
|
||||
[ "$cur" = "GOA" ] && ok "bank currency GOA" || fail "bank currency" "got ${cur:-?}"
|
||||
# wire type / name if present
|
||||
python3 - "$tmp/bank-config.json" <<'PY' 2>/dev/null && ok "bank config JSON object" || fail "bank config JSON"
|
||||
import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
sys.exit(0 if isinstance(d, dict) and d.get("currency") else 1)
|
||||
PY
|
||||
if json_has_alt_unit_names "$tmp/bank-config.json" >/tmp/alt-bank-s.$$ 2>&1; then
|
||||
ok "bank /config alt_unit_names" "$(tr '\n' '; ' </tmp/alt-bank-s.$$ | sed 's/; $//')"
|
||||
else
|
||||
fail "bank /config alt_unit_names" "$(tr '\n' '; ' </tmp/alt-bank-s.$$ | sed 's/; $//')"
|
||||
fi
|
||||
rm -f /tmp/alt-bank-s.$$
|
||||
fi
|
||||
|
||||
# server-side bank
|
||||
if koopa_ssh_ok; then
|
||||
BOUT=$(koopa_ssh_bash 40 <<'REMOTE' || true
|
||||
set +e
|
||||
BANK=$(podman ps --format '{{.Names}}' | grep -iE 'hacktivism-bank|taler-bank' | head -1)
|
||||
[ -z "$BANK" ] && BANK=$(podman ps --format '{{.Names}}' | grep -i bank | head -1)
|
||||
echo "CTR=$BANK"
|
||||
if [ -z "$BANK" ]; then echo "NOCTR"; exit 0; fi
|
||||
code=$(curl -sS -m 5 -o /dev/null -w '%{http_code}' http://127.0.0.1:9012/config 2>/dev/null || echo 000)
|
||||
echo "LOCAL_CONFIG=$code"
|
||||
code2=$(curl -sS -m 5 -o /dev/null -w '%{http_code}' http://127.0.0.1:9012/taler-integration/config 2>/dev/null || echo 000)
|
||||
echo "LOCAL_INT=$code2"
|
||||
if podman exec "$BANK" bash -c 'pgrep -f "MainKt serve|libeufin-bank serve" >/dev/null' 2>/dev/null; then
|
||||
echo "LIBEUFIN=1"
|
||||
else
|
||||
echo "LIBEUFIN=0"
|
||||
fi
|
||||
if podman exec "$BANK" bash -c 'pg_isready -q' 2>/dev/null; then
|
||||
echo "PG=1"
|
||||
else
|
||||
echo "PG=0"
|
||||
fi
|
||||
# in-container health if present
|
||||
if podman exec "$BANK" test -x /usr/local/bin/check_bank-health.sh 2>/dev/null; then
|
||||
podman exec "$BANK" /usr/local/bin/check_bank-health.sh 2>&1 | sed 's/^/HEALTH /' | tail -20
|
||||
echo "HEALTH_EC=${PIPESTATUS[0]}"
|
||||
fi
|
||||
REMOTE
|
||||
)
|
||||
echo "$BOUT" | grep -q '^CTR=.\+' && ok "bank container $(echo "$BOUT" | sed -n 's/^CTR=//p' | head -1)" || fail "bank container" "not found"
|
||||
echo "$BOUT" | grep -q 'LOCAL_CONFIG=200' && ok "bank local :9012/config" || fail "bank local :9012/config"
|
||||
echo "$BOUT" | grep -q 'LOCAL_INT=200' && ok "bank local :9012/taler-integration/config" || fail "bank local integration"
|
||||
echo "$BOUT" | grep -q 'LIBEUFIN=1' && ok "bank libeufin-bank process" || fail "bank libeufin-bank process"
|
||||
echo "$BOUT" | grep -q 'PG=1' && ok "bank postgres ready" || warn "bank postgres" "pg_isready failed"
|
||||
if echo "$BOUT" | grep -q 'HEALTH '; then
|
||||
if echo "$BOUT" | grep -qE 'HEALTH_EC=0|ALL CRITICAL CHECKS PASSED'; then
|
||||
ok "bank check_bank-health.sh"
|
||||
else
|
||||
# health script may false-fail process grep; warn not fail if local config ok
|
||||
warn "bank check_bank-health.sh" "non-zero or incomplete"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
warn "bank server-side" "ssh ${KOOPA_SSH} unavailable"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "sanity · exchange"
|
||||
# ---------------------------------------------------------------------------
|
||||
expect_code "exchange public /config" 200 "$EXCHANGE_PUBLIC/config"
|
||||
expect_code "exchange public /keys" 200 "$EXCHANGE_PUBLIC/keys"
|
||||
expect_code "exchange public /terms" 200 "$EXCHANGE_PUBLIC/terms"
|
||||
|
||||
code=$(http_body "$EXCHANGE_PUBLIC/config" "$tmp/ex-config.json")
|
||||
if [ "$code" = "200" ]; then
|
||||
cur=$(json_currency "$tmp/ex-config.json")
|
||||
[ "$cur" = "GOA" ] && ok "exchange currency GOA" || fail "exchange currency" "got ${cur:-?}"
|
||||
if json_has_alt_unit_names "$tmp/ex-config.json" "GOA" >/tmp/alt-ex-s.$$ 2>&1; then
|
||||
ok "exchange /config alt_unit_names" "$(tr '\n' '; ' </tmp/alt-ex-s.$$ | sed 's/; $//')"
|
||||
else
|
||||
fail "exchange /config alt_unit_names" "$(tr '\n' '; ' </tmp/alt-ex-s.$$ | sed 's/; $//')"
|
||||
fi
|
||||
rm -f /tmp/alt-ex-s.$$
|
||||
fi
|
||||
|
||||
code=$(http_body "$EXCHANGE_PUBLIC/keys" "$tmp/ex-keys.json")
|
||||
if [ "$code" = "200" ]; then
|
||||
python3 - "$tmp/ex-keys.json" <<'PY'
|
||||
import json,sys,time
|
||||
d=json.load(open(sys.argv[1]))
|
||||
sk=d.get("signkeys") or []
|
||||
acc=d.get("accounts") or []
|
||||
den=d.get("denominations") or []
|
||||
# count denoms roughly
|
||||
n=0
|
||||
for g in den:
|
||||
if isinstance(g, dict):
|
||||
n += len(g.get("denoms") or [])
|
||||
print(f"signkeys={len(sk)} accounts={len(acc)} denom_groups={len(den)} denoms~={n}")
|
||||
sys.exit(0 if sk and (acc or n) else 1)
|
||||
PY
|
||||
ec=$?
|
||||
detail=$(python3 - "$tmp/ex-keys.json" <<'PY'
|
||||
import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
sk=d.get("signkeys") or []
|
||||
acc=d.get("accounts") or []
|
||||
den=d.get("denominations") or []
|
||||
n=sum(len(g.get("denoms") or []) for g in den if isinstance(g, dict))
|
||||
print(f"signkeys={len(sk)} accounts={len(acc)} denoms~={n}")
|
||||
PY
|
||||
)
|
||||
[ "$ec" -eq 0 ] && ok "exchange /keys usable ($detail)" || fail "exchange /keys usable" "$detail"
|
||||
fi
|
||||
|
||||
# /wire if exposed
|
||||
wcode=$(http_code "$EXCHANGE_PUBLIC/wire")
|
||||
if [ "$wcode" = "200" ]; then
|
||||
ok "exchange public /wire"
|
||||
else
|
||||
warn "exchange public /wire" "HTTP $wcode (accounts may only be in /keys)"
|
||||
fi
|
||||
|
||||
if koopa_ssh_ok; then
|
||||
EOUT=$(koopa_ssh_bash 40 <<'REMOTE' || true
|
||||
set +e
|
||||
EX=$(podman ps --format '{{.Names}}' | grep -i exchange | head -1)
|
||||
echo "CTR=$EX"
|
||||
code=$(curl -sS -m 5 -o /dev/null -w '%{http_code}' http://127.0.0.1:9011/config 2>/dev/null || echo 000)
|
||||
echo "LOCAL_CONFIG=$code"
|
||||
codek=$(curl -sS -m 8 -o /dev/null -w '%{http_code}' http://127.0.0.1:9011/keys 2>/dev/null || echo 000)
|
||||
echo "LOCAL_KEYS=$codek"
|
||||
if [ -n "$EX" ]; then
|
||||
if podman exec "$EX" bash -c 'pgrep -f taler-exchange-httpd >/dev/null' 2>/dev/null; then
|
||||
echo "HTTPD=1"
|
||||
else
|
||||
echo "HTTPD=0"
|
||||
fi
|
||||
for p in taler-exchange-secmod-rsa taler-exchange-secmod-eddsa taler-exchange-wirewatch taler-exchange-aggregator; do
|
||||
if podman exec "$EX" bash -c "pgrep -f $p >/dev/null" 2>/dev/null; then
|
||||
echo "PROC_$p=1"
|
||||
else
|
||||
echo "PROC_$p=0"
|
||||
fi
|
||||
done
|
||||
if podman exec "$EX" test -x /usr/local/bin/check_exchange-health.sh 2>/dev/null; then
|
||||
SKIP_ENSURE=1 podman exec -e SKIP_ENSURE=1 "$EX" /usr/local/bin/check_exchange-health.sh 2>&1 | sed 's/^/HEALTH /' | tail -25
|
||||
fi
|
||||
fi
|
||||
REMOTE
|
||||
)
|
||||
echo "$EOUT" | grep -q '^CTR=.\+' && ok "exchange container $(echo "$EOUT" | sed -n 's/^CTR=//p' | head -1)" || fail "exchange container"
|
||||
echo "$EOUT" | grep -q 'LOCAL_CONFIG=200' && ok "exchange local :9011/config" || fail "exchange local :9011/config"
|
||||
echo "$EOUT" | grep -q 'LOCAL_KEYS=200' && ok "exchange local :9011/keys" || fail "exchange local :9011/keys"
|
||||
echo "$EOUT" | grep -q 'HTTPD=1' && ok "exchange-httpd process" || warn "exchange-httpd process" "not detected"
|
||||
echo "$EOUT" | grep -q 'PROC_taler-exchange-wirewatch=1' && ok "exchange wirewatch" || warn "exchange wirewatch" "not running"
|
||||
echo "$EOUT" | grep -q 'PROC_taler-exchange-aggregator=1' && ok "exchange aggregator" || warn "exchange aggregator" "not running"
|
||||
else
|
||||
warn "exchange server-side" "ssh unavailable"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
section "sanity · merchant"
|
||||
# ---------------------------------------------------------------------------
|
||||
expect_code "merchant public /config" 200 "$MERCHANT_PUBLIC/config"
|
||||
expect_code "merchant public /webui/" 200 "$MERCHANT_PUBLIC/webui/"
|
||||
|
||||
code=$(http_body "$MERCHANT_PUBLIC/config" "$tmp/mer-config.json")
|
||||
if [ "$code" = "200" ]; then
|
||||
if python3 - "$tmp/mer-config.json" "$EXCHANGE_PUBLIC" <<'PY'
|
||||
import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
want=sys.argv[2].rstrip("/")
|
||||
curs=list((d.get("currencies") or {}).keys())
|
||||
ex=d.get("exchanges") or []
|
||||
urls=[]
|
||||
for e in ex:
|
||||
if isinstance(e, dict):
|
||||
u=e.get("base_url") or e.get("url") or e.get("exchange_base_url") or ""
|
||||
if u: urls.append(u.rstrip("/"))
|
||||
elif isinstance(e, str):
|
||||
urls.append(e.rstrip("/"))
|
||||
ok_goa = "GOA" in curs or any((e.get("currency") if isinstance(e, dict) else None)=="GOA" for e in ex)
|
||||
ok_ex = any(want in u or "exchange.hacktivism.ch" in u for u in urls)
|
||||
print("currencies", curs)
|
||||
print("exchanges", urls[:5])
|
||||
sys.exit(0 if ok_goa and ok_ex else 1)
|
||||
PY
|
||||
then
|
||||
ok "merchant config GOA + exchange.hacktivism.ch"
|
||||
else
|
||||
fail "merchant config GOA + exchange" "see currencies/exchanges"
|
||||
fi
|
||||
if json_has_alt_unit_names "$tmp/mer-config.json" >/tmp/alt-mer-s.$$ 2>&1; then
|
||||
ok "merchant currencies alt_unit_names" "$(tr '\n' '; ' </tmp/alt-mer-s.$$ | sed 's/; $//')"
|
||||
else
|
||||
fail "merchant currencies alt_unit_names" "$(tr '\n' '; ' </tmp/alt-mer-s.$$ | sed 's/; $//')"
|
||||
fi
|
||||
rm -f /tmp/alt-mer-s.$$
|
||||
# Each exchange listed on merchant /config must publish alt_unit_names on its own /config
|
||||
check_merchant_listed_exchanges_alt_units "$tmp/mer-config.json"
|
||||
fi
|
||||
|
||||
# demo instance reachable?
|
||||
INST="${MERCHANT_INSTANCE}"
|
||||
icode=$(http_code "$MERCHANT_PUBLIC/instances/${INST}/config")
|
||||
if [ "$icode" = "200" ]; then
|
||||
ok "merchant instance ${INST} /config"
|
||||
else
|
||||
# some deployments use private only
|
||||
warn "merchant instance ${INST} /config" "HTTP $icode"
|
||||
fi
|
||||
|
||||
if koopa_ssh_ok; then
|
||||
MOUT=$(koopa_ssh_bash 40 <<'REMOTE' || true
|
||||
set +e
|
||||
MER=$(podman ps --format '{{.Names}}' | grep -E '^taler-hacktivism$' | head -1)
|
||||
[ -z "$MER" ] && MER=$(podman ps --format '{{.Names}}' | grep -iE 'merchant|hacktivism' | grep -viE 'bank|exchange' | head -1)
|
||||
echo "CTR=$MER"
|
||||
code=$(curl -skS -m 5 -o /dev/null -w '%{http_code}' https://127.0.0.1:9010/config 2>/dev/null || echo 000)
|
||||
echo "LOCAL_CONFIG=$code"
|
||||
if [ -n "$MER" ]; then
|
||||
if podman exec "$MER" bash -c 'pgrep -f taler-merchant-httpd >/dev/null' 2>/dev/null; then
|
||||
echo "HTTPD=1"
|
||||
else
|
||||
echo "HTTPD=0"
|
||||
fi
|
||||
if podman exec "$MER" test -x /usr/local/bin/check_merchant-health.sh 2>/dev/null; then
|
||||
SKIP_ENSURE=1 podman exec -e SKIP_ENSURE=1 "$MER" /usr/local/bin/check_merchant-health.sh 2>&1 | sed 's/^/HEALTH /' | tail -30
|
||||
fi
|
||||
fi
|
||||
REMOTE
|
||||
)
|
||||
echo "$MOUT" | grep -q '^CTR=.\+' && ok "merchant container $(echo "$MOUT" | sed -n 's/^CTR=//p' | head -1)" || fail "merchant container"
|
||||
echo "$MOUT" | grep -q 'LOCAL_CONFIG=200' && ok "merchant local :9010/config" || fail "merchant local :9010/config"
|
||||
echo "$MOUT" | grep -q 'HTTPD=1' && ok "merchant-httpd process" || warn "merchant-httpd process" "not detected"
|
||||
if echo "$MOUT" | grep -q 'HEALTH '; then
|
||||
if echo "$MOUT" | grep -qiE 'ALL CRITICAL|HEALTH_EC=0|\[OK\]'; then
|
||||
ok "merchant check_merchant-health.sh (sample OK)"
|
||||
else
|
||||
warn "merchant check_merchant-health.sh" "see remote output"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
warn "merchant server-side" "ssh unavailable"
|
||||
fi
|
||||
|
||||
summary
|
||||
128
scripts/taler-monitoring/check_server.sh
Executable file
128
scripts/taler-monitoring/check_server.sh
Executable file
|
|
@ -0,0 +1,128 @@
|
|||
#!/usr/bin/env bash
|
||||
# Server-side component checks on koopa (via SSH).
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
# Area server-### — host/container ports via SSH
|
||||
set_area server
|
||||
section "server · ssh ${KOOPA_SSH}"
|
||||
|
||||
if ! koopa_ssh_ok; then
|
||||
fail "ssh ${KOOPA_SSH}" "unreachable within ${SSH_CONNECT_TIMEOUT}s (SKIP_SSH=1 to skip)"
|
||||
summary
|
||||
exit 1
|
||||
fi
|
||||
ok "ssh ${KOOPA_SSH}"
|
||||
|
||||
# Run a remote script; collect structured lines
|
||||
REMOTE=$(koopa_ssh_bash "${SSH_CMD_TIMEOUT}" <<'REMOTE'
|
||||
set +e
|
||||
report() { printf 'R|%s|%s|%s\n' "$1" "$2" "$3"; }
|
||||
|
||||
# containers
|
||||
for name in taler-hacktivism-bank taler-hacktivism-exchange-ansible taler-hacktivism; do
|
||||
if podman ps --format '{{.Names}}' 2>/dev/null | grep -qx "$name"; then
|
||||
st=$(podman ps --filter "name=^${name}$" --format '{{.Status}}' | head -1)
|
||||
report OK "container $name" "$st"
|
||||
else
|
||||
# soft match
|
||||
hit=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -E "$name|bank|exchange|hacktivism" | head -3 | tr '\n' ' ')
|
||||
if podman ps --format '{{.Names}}' 2>/dev/null | grep -q bank && [ "$name" = taler-hacktivism-bank ]; then
|
||||
bn=$(podman ps --format '{{.Names}}' | grep -i bank | head -1)
|
||||
report OK "container bank ($bn)" "$(podman ps --filter name="$bn" --format '{{.Status}}' | head -1)"
|
||||
elif podman ps --format '{{.Names}}' 2>/dev/null | grep -qi exchange && echo "$name" | grep -qi exchange; then
|
||||
en=$(podman ps --format '{{.Names}}' | grep -i exchange | head -1)
|
||||
report OK "container exchange ($en)" "$(podman ps --filter name="$en" --format '{{.Status}}' | head -1)"
|
||||
elif podman ps --format '{{.Names}}' 2>/dev/null | grep -qx taler-hacktivism && [ "$name" = taler-hacktivism ]; then
|
||||
report OK "container taler-hacktivism" "$(podman inspect -f '{{.State.Status}}' taler-hacktivism 2>/dev/null)"
|
||||
else
|
||||
report FAIL "container $name" "not running (seen: $hit)"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# local HTTP on pasta ports
|
||||
for spec in \
|
||||
"bank-api|http://127.0.0.1:9012/config|200" \
|
||||
"bank-integration|http://127.0.0.1:9012/taler-integration/config|200" \
|
||||
"landing|http://127.0.0.1:9013/intro/|200" \
|
||||
"exchange|http://127.0.0.1:9011/config|200" \
|
||||
"merchant|https://127.0.0.1:9010/config|200"
|
||||
do
|
||||
IFS='|' read -r id url want <<<"$spec"
|
||||
code=$(curl -skS -m 5 -o /tmp/mon-s.out -w '%{http_code}' "$url" 2>/dev/null || echo 000)
|
||||
if [ "$code" = "$want" ]; then
|
||||
report OK "local $id : ${url#*//}" "HTTP $code"
|
||||
else
|
||||
report FAIL "local $id" "HTTP $code want $want"
|
||||
fi
|
||||
done
|
||||
|
||||
# processes inside bank
|
||||
BANK=$(podman ps --format '{{.Names}}' | grep -iE 'bank|hacktivism-bank' | head -1)
|
||||
if [ -n "$BANK" ]; then
|
||||
if podman exec "$BANK" bash -c 'pgrep -f "MainKt serve|libeufin-bank serve" >/dev/null' 2>/dev/null; then
|
||||
report OK "libeufin-bank process" "in $BANK"
|
||||
else
|
||||
report FAIL "libeufin-bank process" "not running in $BANK"
|
||||
fi
|
||||
if podman exec "$BANK" bash -c 'pg_isready -q' 2>/dev/null; then
|
||||
report OK "postgres (bank)" "ready"
|
||||
else
|
||||
report WARN "postgres (bank)" "pg_isready failed"
|
||||
fi
|
||||
if podman exec "$BANK" bash -c 'pgrep -x nginx >/dev/null' 2>/dev/null; then
|
||||
report OK "nginx landing" "in $BANK"
|
||||
else
|
||||
report WARN "nginx landing" "not seen in $BANK"
|
||||
fi
|
||||
else
|
||||
report FAIL "bank container" "none"
|
||||
fi
|
||||
|
||||
# exchange process / systemd if any
|
||||
EX=$(podman ps --format '{{.Names}}' | grep -i exchange | head -1)
|
||||
if [ -n "$EX" ]; then
|
||||
if podman exec "$EX" bash -c 'pgrep -f taler-exchange-httpd >/dev/null || systemctl is-active taler-exchange-httpd 2>/dev/null | grep -q active' 2>/dev/null; then
|
||||
report OK "exchange-httpd" "in $EX"
|
||||
else
|
||||
# config answering is enough
|
||||
report WARN "exchange-httpd process" "not detected; port check above"
|
||||
fi
|
||||
fi
|
||||
|
||||
MER=$(podman ps --format '{{.Names}}' | grep -E '^taler-hacktivism$' | head -1)
|
||||
[ -z "$MER" ] && MER=$(podman ps --format '{{.Names}}' | grep -i merchant | head -1)
|
||||
if [ -n "$MER" ]; then
|
||||
if podman exec "$MER" bash -c 'pgrep -f taler-merchant-httpd >/dev/null || true; curl -sk -m 3 -o /dev/null -w %{http_code} https://127.0.0.1:9010/config' 2>/dev/null | grep -q 200; then
|
||||
report OK "merchant-httpd" "responds in $MER"
|
||||
else
|
||||
report WARN "merchant-httpd" "check manually in $MER"
|
||||
fi
|
||||
fi
|
||||
|
||||
# caddy on host
|
||||
if systemctl is-active caddy >/dev/null 2>&1 || pgrep -x caddy >/dev/null 2>&1; then
|
||||
report OK "caddy" "active"
|
||||
else
|
||||
report WARN "caddy" "not detected as active"
|
||||
fi
|
||||
REMOTE
|
||||
)
|
||||
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
R\|*)
|
||||
IFS='|' read -r _ st label detail <<<"$line"
|
||||
case "$st" in
|
||||
OK) ok "$label${detail:+ ($detail)}" ;;
|
||||
FAIL) fail "$label" "$detail" ;;
|
||||
WARN) warn "$label" "$detail" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
done <<<"$REMOTE"
|
||||
|
||||
summary
|
||||
636
scripts/taler-monitoring/check_urls.sh
Executable file
636
scripts/taler-monitoring/check_urls.sh
Executable file
|
|
@ -0,0 +1,636 @@
|
|||
#!/usr/bin/env bash
|
||||
# Outside-in public HTTPS checks (no SSH).
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
# Area www-### — public HTTPS (outside-in)
|
||||
set_area www
|
||||
section "www · public URLs · ${TALER_DOMAIN:-?} (outside-in, no SSH)"
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
check_url() {
|
||||
local label="$1" expect="$2" url="$3"
|
||||
local code
|
||||
code=$(http_code "$url")
|
||||
case ",$expect," in
|
||||
*",$code,"*) ok "$label $url" ;;
|
||||
*) fail "$label $url" "got $code want $expect" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Soft check: OK on expect, WARN on foreign stack if down, ERROR on local stack
|
||||
check_url_soft() {
|
||||
local label="$1" expect="$2" url="$3"
|
||||
local code
|
||||
code=$(http_code "$url")
|
||||
case ",$expect," in
|
||||
*",$code,"*) ok "$label $url" ;;
|
||||
*)
|
||||
if [ "${LOCAL_STACK:-1}" = "0" ]; then
|
||||
warn "$label $url" "got $code (optional on remote domain)"
|
||||
else
|
||||
fail "$label $url" "got $code want $expect"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
expect_currency() {
|
||||
local label="$1" file="$2" want="${EXPECT_CURRENCY:-}"
|
||||
local cur
|
||||
cur=$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("currency",""))' "$file" 2>/dev/null || true)
|
||||
if [ -z "$want" ]; then
|
||||
info "$label currency" "${cur:-?}"
|
||||
return
|
||||
fi
|
||||
if [ "$cur" = "$want" ]; then
|
||||
ok "$label currency=$want"
|
||||
else
|
||||
fail "$label currency" "got ${cur:-?} want $want"
|
||||
fi
|
||||
}
|
||||
|
||||
# Legal docs: /terms and /privacy must be HTTP 200 with a real document body
|
||||
# (not empty, not "not configured", not JSON API error).
|
||||
# $1=label $2=url $3=optional needle regex (case-insensitive) for local stack
|
||||
check_legal_doc() {
|
||||
local label="$1" url="$2" needle="${3:-}"
|
||||
local f code soft
|
||||
soft=0
|
||||
[ "${LOCAL_STACK:-1}" = "0" ] && soft=1
|
||||
f=$(mktemp)
|
||||
code=$(curl -skS --max-redirs 5 -L -m "${TIMEOUT}" \
|
||||
-H "Accept: text/html,text/markdown,text/plain,*/*" \
|
||||
-o "$f" -w '%{http_code}' "$url" 2>/dev/null || echo 000)
|
||||
if [ "$code" != "200" ]; then
|
||||
rm -f "$f"
|
||||
if [ "$soft" = "1" ]; then
|
||||
warn "$label" "HTTP $code — $url"
|
||||
else
|
||||
fail "$label" "HTTP $code — $url"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
if [ ! -s "$f" ]; then
|
||||
rm -f "$f"
|
||||
fail "$label" "empty body — $url"
|
||||
return
|
||||
fi
|
||||
# merchant returns plain "not configured" when PRIVACY_ETAG missing
|
||||
if grep -qiE '^(not configured)\s*$' "$f" 2>/dev/null \
|
||||
|| grep -qiE '"code"\s*:\s*21' "$f" 2>/dev/null; then
|
||||
rm -f "$f"
|
||||
fail "$label" "not configured / API error — $url"
|
||||
return
|
||||
fi
|
||||
if [ -n "$needle" ] && [ "${LOCAL_STACK:-1}" = "1" ]; then
|
||||
if ! grep -qiE "$needle" "$f" 2>/dev/null; then
|
||||
warn "$label content" "missing /$needle/ — $url"
|
||||
rm -f "$f"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
ok "$label" "HTTP 200 · $(wc -c <"$f" | tr -d ' ') bytes"
|
||||
rm -f "$f"
|
||||
}
|
||||
|
||||
# --- exchange (core; always required) --- www-001 …
|
||||
check_url "exchange /config" 200 "$EXCHANGE_PUBLIC/config"
|
||||
code=$(http_body "$EXCHANGE_PUBLIC/config" "$tmp/ec.json")
|
||||
if [ "$code" = "200" ]; then
|
||||
expect_currency "exchange" "$tmp/ec.json"
|
||||
# currency_specification.alt_unit_names (wallet codec)
|
||||
if json_has_alt_unit_names "$tmp/ec.json" "${EXPECT_CURRENCY:-}" >/tmp/alt-ex.$$ 2>&1; then
|
||||
ok "exchange /config alt_unit_names" "$(tr '\n' '; ' </tmp/alt-ex.$$ | sed 's/; $//')"
|
||||
else
|
||||
fail "exchange /config alt_unit_names" "$(tr '\n' '; ' </tmp/alt-ex.$$ | sed 's/; $//')"
|
||||
fi
|
||||
rm -f /tmp/alt-ex.$$
|
||||
fi
|
||||
check_url "exchange /keys" 200 "$EXCHANGE_PUBLIC/keys"
|
||||
# /keys should also expose currency_specification.alt_unit_names when present
|
||||
code=$(http_body "$EXCHANGE_PUBLIC/keys" "$tmp/ek.json")
|
||||
if [ "$code" = "200" ]; then
|
||||
if python3 - "$tmp/ek.json" <<'PY'
|
||||
import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
cs=d.get("currency_specification") or {}
|
||||
au=cs.get("alt_unit_names") if isinstance(cs,dict) else None
|
||||
if not isinstance(au, dict) or "0" not in au:
|
||||
# older keys without embedded spec: not a hard fail if /config is good
|
||||
sys.exit(2)
|
||||
sys.exit(0)
|
||||
PY
|
||||
then
|
||||
ok "exchange /keys alt_unit_names"
|
||||
else
|
||||
ec=$?
|
||||
if [ "$ec" = "2" ]; then
|
||||
warn "exchange /keys alt_unit_names" "no currency_specification in /keys (ok if /config has it)"
|
||||
else
|
||||
fail "exchange /keys alt_unit_names"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
check_url_soft "exchange /intro/" 200 "$EXCHANGE_PUBLIC/intro/"
|
||||
# Root should land on intro (302/301 then 200 on follow is checked separately)
|
||||
check_url_soft "exchange /" 302,301,200 "$EXCHANGE_PUBLIC/"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Performance — outside-in public HTTPS latency (this runner, not loopback)
|
||||
# Same spirit as bank landing-stats public probes; measured here from outside.
|
||||
# ---------------------------------------------------------------------------
|
||||
section "www · performance · public HTTPS latency (outside-in)"
|
||||
|
||||
# Latency thresholds (ms), outside-in from this runner.
|
||||
# ≥ PERF_WARN_MS → WARN. ≥ PERF_FAIL_MS → ERROR (suite fails).
|
||||
PERF_WARN_MS="${PERF_WARN_MS:-8000}"
|
||||
PERF_FAIL_MS="${PERF_FAIL_MS:-20000}"
|
||||
|
||||
# Measure one URL: require HTTP expect (default 200), report time_total in ms.
|
||||
# $1=label $2=url $3=optional expected codes (default 200)
|
||||
check_perf() {
|
||||
local label="$1" url="$2" expect="${3:-200}"
|
||||
local out code t_s ms
|
||||
out=$(curl -skS --max-redirs 3 -L -m "${PERF_CURL_TIMEOUT:-25}" \
|
||||
-o /dev/null -w '%{http_code} %{time_total}' "$url" 2>/dev/null || echo "000 0")
|
||||
code=$(printf '%s' "$out" | awk '{print $1}')
|
||||
t_s=$(printf '%s' "$out" | awk '{print $2}')
|
||||
ms=$(awk -v t="${t_s:-0}" 'BEGIN{
|
||||
ms=(t+0)*1000
|
||||
if (ms>0 && ms<1) ms=1
|
||||
printf "%d", int(ms+0.5)
|
||||
}')
|
||||
case ",$expect," in
|
||||
*",$code,"*)
|
||||
if [ "$ms" -ge "${PERF_FAIL_MS}" ] 2>/dev/null; then
|
||||
fail "$label" "HTTP $code · ${ms} ms ≥ fail ${PERF_FAIL_MS} ms · $url"
|
||||
elif [ "$ms" -ge "${PERF_WARN_MS}" ] 2>/dev/null; then
|
||||
warn "$label" "HTTP $code · ${ms} ms ≥ warn ${PERF_WARN_MS} ms · $url"
|
||||
else
|
||||
ok "$label" "HTTP $code · ${ms} ms · $url"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ]; then
|
||||
fail "$label" "HTTP $code want $expect · ${ms} ms · $url"
|
||||
else
|
||||
warn "$label" "HTTP $code want $expect · ${ms} ms · $url"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Bank first (wallet-critical paths before UI chrome)
|
||||
check_perf "perf bank /taler-integration/config" "$BANK_PUBLIC/taler-integration/config"
|
||||
check_perf "perf bank /config" "$BANK_PUBLIC/config"
|
||||
check_perf "perf bank /intro/" "$BANK_PUBLIC/intro/"
|
||||
check_perf "perf bank /intro/stats.json" "$BANK_PUBLIC/intro/stats.json" 200
|
||||
check_perf "perf bank /webui/" "$BANK_PUBLIC/webui/" 200,301,302
|
||||
|
||||
# Exchange
|
||||
check_perf "perf exchange /config" "$EXCHANGE_PUBLIC/config"
|
||||
check_perf "perf exchange /keys" "$EXCHANGE_PUBLIC/keys"
|
||||
check_perf "perf exchange /intro/" "$EXCHANGE_PUBLIC/intro/"
|
||||
|
||||
# Merchant
|
||||
check_perf "perf merchant /config" "$MERCHANT_PUBLIC/config"
|
||||
check_perf "perf merchant /webui/" "$MERCHANT_PUBLIC/webui/" 200,301,302
|
||||
check_perf "perf merchant /intro/" "$MERCHANT_PUBLIC/intro/"
|
||||
|
||||
info "perf note" "measured from this host (outside-in); not container loopback"
|
||||
|
||||
|
||||
# Terms + privacy (legal docs)
|
||||
check_legal_doc "exchange /terms" "$EXCHANGE_PUBLIC/terms" "terms|GOA|exploration|FADP|revDSG|privacy"
|
||||
check_legal_doc "exchange /privacy" "$EXCHANGE_PUBLIC/privacy" "privacy|FADP|revDSG|data|GOA|exploration"
|
||||
# trailing slash: 200 or redirect to bare path
|
||||
code=$(http_code "$EXCHANGE_PUBLIC/terms/")
|
||||
case "$code" in
|
||||
200|301|302) ok "exchange /terms/" "HTTP $code" ;;
|
||||
*)
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ]; then warn "exchange /terms/" "HTTP $code"
|
||||
else warn "exchange /terms/" "HTTP $code"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# --- bank ---
|
||||
if [ "${LOCAL_STACK:-1}" = "0" ]; then
|
||||
check_url_soft "bank /config" 200 "$BANK_PUBLIC/config"
|
||||
else
|
||||
check_url "bank /config" 200 "$BANK_PUBLIC/config"
|
||||
fi
|
||||
code=$(http_body "$BANK_PUBLIC/config" "$tmp/bc.json")
|
||||
if [ "$code" = "200" ]; then
|
||||
expect_currency "bank" "$tmp/bc.json"
|
||||
if json_has_alt_unit_names "$tmp/bc.json" >/tmp/alt-bank.$$ 2>&1; then
|
||||
ok "bank /config alt_unit_names" "$(tr '\n' '; ' </tmp/alt-bank.$$ | sed 's/; $//')"
|
||||
else
|
||||
fail "bank /config alt_unit_names" "$(tr '\n' '; ' </tmp/alt-bank.$$ | sed 's/; $//')"
|
||||
fi
|
||||
rm -f /tmp/alt-bank.$$
|
||||
# Wallet probes this; GET must be 200 (POST→405 is normal)
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ]; then
|
||||
check_url "bank /taler-integration/config" 200 "$BANK_PUBLIC/taler-integration/config"
|
||||
else
|
||||
check_url_soft "bank /taler-integration/config" 200 "$BANK_PUBLIC/taler-integration/config"
|
||||
fi
|
||||
check_url_soft "bank /webui/" 200 "$BANK_PUBLIC/webui/"
|
||||
check_url_soft "bank /intro/" 200 "$BANK_PUBLIC/intro/"
|
||||
check_url_soft "bank /" 302,301,200 "$BANK_PUBLIC/"
|
||||
# Auto-account: credentials + shared-pool taler://withdraw (like step 2)
|
||||
aa_code=$(http_body "$BANK_PUBLIC/intro/auto-account.json" "$tmp/aa.json")
|
||||
case "$aa_code" in
|
||||
200)
|
||||
if python3 - "$tmp/aa.json" <<'PY'
|
||||
import json, re, sys
|
||||
from urllib.parse import urlparse
|
||||
d = json.load(open(sys.argv[1]))
|
||||
if not d.get("ok"):
|
||||
print("ok!=true"); sys.exit(1)
|
||||
if "payto_uri" in d and d.get("payto_uri"):
|
||||
print("payto_uri must not be present"); sys.exit(1)
|
||||
wuri = d.get("taler_withdraw_uri") or d.get("qr_payload") or ""
|
||||
wm = re.match(r"^taler://withdraw/([^/]+)/taler-integration/([0-9a-fA-F-]+)$", wuri)
|
||||
if not wm:
|
||||
print("need taler://withdraw/HOST:PORT/taler-integration/ID:", wuri[:120]); sys.exit(1)
|
||||
if ":" not in wm.group(1):
|
||||
print("withdraw missing port:", wm.group(1)); sys.exit(1)
|
||||
webui = d.get("login_url") or d.get("webui") or d.get("account_url") or ""
|
||||
u = urlparse(webui)
|
||||
if u.scheme not in ("http", "https") or "webui" not in (u.path or ""):
|
||||
print("login webui missing:", webui[:80]); sys.exit(1)
|
||||
print("user=%s withdraw=%s login=%s" % (d.get("username"), wm.group(1), webui))
|
||||
sys.exit(0)
|
||||
PY
|
||||
then
|
||||
ok "bank /intro/auto-account.json" "$(python3 -c 'import json;d=json.load(open("'"$tmp/aa.json"'"));print(d.get("username",""),"·",(d.get("taler_withdraw_uri") or "")[:72])' 2>/dev/null || true)"
|
||||
else
|
||||
fail "bank /intro/auto-account.json" "invalid withdraw/login (HTTP body bad)"
|
||||
fi
|
||||
;;
|
||||
405|501|404|502|503|000)
|
||||
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"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Bank legal docs (landing nginx via Caddy /terms* /privacy* or /intro/*)
|
||||
check_legal_doc "bank /terms" "$BANK_PUBLIC/terms" "terms|GOA|exploration|bank|FADP|revDSG"
|
||||
# Prefer /privacy; fall back to /intro/privacy.html for older deploys
|
||||
code=$(http_code "$BANK_PUBLIC/privacy")
|
||||
if [ "$code" = "200" ]; then
|
||||
check_legal_doc "bank /privacy" "$BANK_PUBLIC/privacy" "privacy|FADP|revDSG|data|GOA|bank"
|
||||
else
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ]; then
|
||||
check_legal_doc "bank /privacy (or /intro/privacy.html)" \
|
||||
"$BANK_PUBLIC/intro/privacy.html" "privacy|FADP|revDSG|data|GOA|bank"
|
||||
# still report bare /privacy failure for local
|
||||
warn "bank /privacy" "HTTP $code — prefer Caddy handle /privacy* → landing"
|
||||
else
|
||||
check_url_soft "bank /privacy" 200 "$BANK_PUBLIC/privacy"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- merchant ---
|
||||
if [ "${LOCAL_STACK:-1}" = "0" ]; then
|
||||
check_url_soft "merchant /config" 200 "$MERCHANT_PUBLIC/config"
|
||||
else
|
||||
check_url "merchant /config" 200 "$MERCHANT_PUBLIC/config"
|
||||
fi
|
||||
code=$(http_body "$MERCHANT_PUBLIC/config" "$tmp/mc.json")
|
||||
if [ "$code" = "200" ]; then
|
||||
want="${EXPECT_CURRENCY:-}"
|
||||
if python3 - "$tmp/mc.json" "$want" <<'PY'
|
||||
import json,sys
|
||||
try:
|
||||
d=json.load(open(sys.argv[1]))
|
||||
except Exception:
|
||||
sys.exit(2)
|
||||
want=sys.argv[2]
|
||||
if not want:
|
||||
sys.exit(0)
|
||||
curs=list((d.get("currencies") or {}).keys())
|
||||
ex=d.get("exchanges") or []
|
||||
ok = want in curs or any((e.get("currency") if isinstance(e,dict) else None)==want for e in ex)
|
||||
if not ok and isinstance(d.get("currency"), str):
|
||||
ok = d["currency"]==want
|
||||
sys.exit(0 if ok else 1)
|
||||
PY
|
||||
then
|
||||
ok "merchant /config (${want:-currency} ok)"
|
||||
else
|
||||
ec=$?
|
||||
if [ "$ec" = "2" ]; then
|
||||
warn "merchant /config" "non-JSON body"
|
||||
elif [ -n "$want" ]; then
|
||||
fail "merchant /config currency" "want $want"
|
||||
else
|
||||
info "merchant /config" "ok"
|
||||
fi
|
||||
fi
|
||||
# merchant-local currency maps (GOA + CHF, …)
|
||||
if json_has_alt_unit_names "$tmp/mc.json" >/tmp/alt-mer.$$ 2>&1; then
|
||||
ok "merchant /config currencies alt_unit_names" "$(tr '\n' '; ' </tmp/alt-mer.$$ | sed 's/; $//')"
|
||||
else
|
||||
fail "merchant /config currencies alt_unit_names" "$(tr '\n' '; ' </tmp/alt-mer.$$ | sed 's/; $//')"
|
||||
fi
|
||||
rm -f /tmp/alt-mer.$$
|
||||
# Follow each exchange listed in merchant /config and require its /config alt_unit_names
|
||||
check_merchant_listed_exchanges_alt_units "$tmp/mc.json"
|
||||
check_url_soft "merchant /intro/" 200 "$MERCHANT_PUBLIC/intro/"
|
||||
check_url_soft "merchant /webui/" 200 "$MERCHANT_PUBLIC/webui/"
|
||||
check_url_soft "merchant /" 302,301,200 "$MERCHANT_PUBLIC/"
|
||||
fi
|
||||
|
||||
# Merchant legal docs
|
||||
check_legal_doc "merchant /terms" "$MERCHANT_PUBLIC/terms" "terms|dual|GOA|CHF|explorational|merchant"
|
||||
check_legal_doc "merchant /privacy" "$MERCHANT_PUBLIC/privacy" "privacy|FADP|revDSG|data|GOA|CHF|merchant"
|
||||
code=$(http_code "$MERCHANT_PUBLIC/terms/")
|
||||
case "$code" in
|
||||
200|301|302) ok "merchant /terms/" "HTTP $code" ;;
|
||||
*) warn "merchant /terms/" "HTTP $code (expect 302 → /terms)" ;;
|
||||
esac
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Landing pages: every HTTPS link exposed on bank / merchant / exchange intros
|
||||
# + required static assets + bank withdraw mint (taler://withdraw only)
|
||||
# ---------------------------------------------------------------------------
|
||||
section "www · landing exposed links · bank / merchant / exchange"
|
||||
|
||||
# Known-good landing static paths (relative to each host base)
|
||||
# Checked even if HTML parse misses them.
|
||||
check_landing_asset() {
|
||||
local label="$1" url="$2" soft="${3:-0}"
|
||||
local code
|
||||
code=$(http_code "$url")
|
||||
case "$code" in
|
||||
200) ok "$label" "HTTP 200 · $url" ;;
|
||||
301|302|303|307|308)
|
||||
# follow once for assets that redirect
|
||||
code=$(curl -skS --max-redirs 3 -L -m "${TIMEOUT}" -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || echo 000)
|
||||
if [ "$code" = "200" ]; then
|
||||
ok "$label" "HTTP redirect→200 · $url"
|
||||
elif [ "$soft" = "1" ]; then
|
||||
warn "$label" "HTTP $code — $url"
|
||||
else
|
||||
fail "$label" "HTTP $code after redirect — $url"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if [ "$soft" = "1" ]; then
|
||||
warn "$label" "HTTP $code — $url"
|
||||
else
|
||||
fail "$label" "HTTP $code — $url"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Soft external (app stores / upstream docs): WARN if down, never ERROR
|
||||
check_external_soft() {
|
||||
local label="$1" url="$2"
|
||||
local code
|
||||
code=$(curl -skS --max-redirs 5 -L -m "${TIMEOUT}" -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || echo 000)
|
||||
case "$code" in
|
||||
200|204|301|302|303|307|308) ok "$label" "HTTP $code · $url" ;;
|
||||
*) warn "$label" "HTTP $code (external soft) · $url" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Parse one landing HTML: collect absolute https + root-relative href/src;
|
||||
# resolve against base; classify own-stack vs external.
|
||||
# Writes lists: $1.own $1.ext (one URL per line)
|
||||
extract_landing_urls() {
|
||||
local base="$1" html="$2" out_prefix="$3"
|
||||
python3 - "$base" "$html" "$out_prefix" <<'PY'
|
||||
import re, sys
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
base, html_path, out = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
html = open(html_path, encoding="utf-8", errors="replace").read()
|
||||
base = base.rstrip("/") + "/"
|
||||
parsed_base = urlparse(base)
|
||||
# Hard-check only the three public Taler hosts for this stack (not git.* etc.)
|
||||
own_hosts = {
|
||||
(parsed_base.hostname or "").lower(),
|
||||
"bank.hacktivism.ch",
|
||||
"exchange.hacktivism.ch",
|
||||
"taler.hacktivism.ch",
|
||||
}
|
||||
# include configured public hosts when domain differs (demo / ops)
|
||||
for envu in (
|
||||
__import__("os").environ.get("BANK_PUBLIC", ""),
|
||||
__import__("os").environ.get("EXCHANGE_PUBLIC", ""),
|
||||
__import__("os").environ.get("MERCHANT_PUBLIC", ""),
|
||||
):
|
||||
h = urlparse(envu).hostname if envu else None
|
||||
if h:
|
||||
own_hosts.add(h.lower())
|
||||
|
||||
raw = set()
|
||||
for m in re.finditer(
|
||||
r'''(?:href|src|content)=["']([^"'#]+)["']''', html, re.I
|
||||
):
|
||||
raw.add(m.group(1).strip())
|
||||
# bare absolute URLs in scripts (fetch, template strings)
|
||||
for m in re.finditer(r'''https://[^\s"'<>\\]+''', html):
|
||||
u = m.group(0).rstrip("\\).,;'\"")
|
||||
# strip trailing punctuation leftovers
|
||||
while u and u[-1] in ".,);]}\"'":
|
||||
u = u[:-1]
|
||||
if u.startswith("https://"):
|
||||
raw.add(u)
|
||||
|
||||
own, ext = set(), set()
|
||||
skip_prefix = ("data:", "javascript:", "mailto:", "taler://", "blob:")
|
||||
skip_exact = {"website", "summary_large_image", "image/png", "en_US"}
|
||||
for r in raw:
|
||||
if not r or r in skip_exact:
|
||||
continue
|
||||
if r.startswith(skip_prefix):
|
||||
continue
|
||||
# meta content noise
|
||||
if re.fullmatch(r"\d+", r) or r.startswith("width="):
|
||||
continue
|
||||
if " " in r and not r.startswith("http"):
|
||||
continue
|
||||
if r.startswith("//"):
|
||||
absu = "https:" + r
|
||||
elif r.startswith("http://") or r.startswith("https://"):
|
||||
absu = r
|
||||
elif r.startswith("/"):
|
||||
absu = urljoin(base, r)
|
||||
else:
|
||||
# relative asset
|
||||
if "/" in r or r.endswith((".js", ".css", ".png", ".svg", ".html", ".json", ".uri")):
|
||||
absu = urljoin(base + "intro/", r)
|
||||
else:
|
||||
continue
|
||||
# drop query-only noise / anchors already stripped
|
||||
p = urlparse(absu)
|
||||
if p.scheme not in ("http", "https"):
|
||||
continue
|
||||
# normalize: drop fragment
|
||||
absu = absu.split("#", 1)[0]
|
||||
host = (p.hostname or "").lower()
|
||||
# og image query ok
|
||||
if host in own_hosts:
|
||||
own.add(absu)
|
||||
else:
|
||||
ext.add(absu)
|
||||
|
||||
open(out + ".own", "w").write("\n".join(sorted(own)) + ("\n" if own else ""))
|
||||
open(out + ".ext", "w").write("\n".join(sorted(ext)) + ("\n" if ext else ""))
|
||||
print(f"own={len(own)} ext={len(ext)}")
|
||||
PY
|
||||
}
|
||||
|
||||
check_one_landing() {
|
||||
local name="$1" base="$2"
|
||||
local html="$tmp/landing-${name}.html"
|
||||
local pref="$tmp/urls-${name}"
|
||||
local code n own_n ext_n
|
||||
code=$(http_body "${base}/intro/" "$html")
|
||||
if [ "$code" != "200" ]; then
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ]; then
|
||||
fail "landing ${name} /intro/" "HTTP $code"
|
||||
else
|
||||
warn "landing ${name} /intro/" "HTTP $code"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
ok "landing ${name} /intro/" "HTTP 200 · $(wc -c <"$html" | tr -d ' ') bytes"
|
||||
|
||||
# Required static assets (hard on local)
|
||||
check_landing_asset "landing ${name} qrcode.min.js" "${base}/intro/qrcode.min.js"
|
||||
check_landing_asset "landing ${name} og-goa-shop.png" "${base}/intro/og-goa-shop.png"
|
||||
check_landing_asset "landing ${name} qr-logo.png" "${base}/intro/qr-logo.png" 1
|
||||
|
||||
n=$(extract_landing_urls "$base" "$html" "$pref" 2>/dev/null || echo "own=0 ext=0")
|
||||
info "landing ${name} link extract" "$n"
|
||||
own_n=0
|
||||
ext_n=0
|
||||
[ -f "${pref}.own" ] && own_n=$(grep -c . "${pref}.own" 2>/dev/null || echo 0)
|
||||
[ -f "${pref}.ext" ] && ext_n=$(grep -c . "${pref}.ext" 2>/dev/null || echo 0)
|
||||
if [ "${own_n:-0}" -lt 1 ]; then
|
||||
fail "landing ${name} own-stack links" "none extracted from HTML"
|
||||
else
|
||||
ok "landing ${name} own-stack links" "${own_n} URLs to probe"
|
||||
fi
|
||||
|
||||
# Probe every own-stack URL from the page
|
||||
if [ -f "${pref}.own" ]; then
|
||||
while IFS= read -r u; do
|
||||
[ -n "$u" ] || continue
|
||||
# skip mint endpoints that create resources on GET if any (auto-account creates accounts)
|
||||
case "$u" in
|
||||
*/intro/auto-account.json)
|
||||
# shape checked separately; still require 200 GET
|
||||
;;
|
||||
esac
|
||||
code=$(http_code "$u")
|
||||
case "$code" in
|
||||
200) ok "landing ${name} link" "HTTP 200 · $u" ;;
|
||||
301|302|303|307|308)
|
||||
code=$(curl -skS --max-redirs 5 -L -m "${TIMEOUT}" -o /dev/null -w '%{http_code}' "$u" 2>/dev/null || echo 000)
|
||||
if [ "$code" = "200" ]; then
|
||||
ok "landing ${name} link" "redirect→200 · $u"
|
||||
else
|
||||
fail "landing ${name} link" "HTTP $code after redirect · $u"
|
||||
fi
|
||||
;;
|
||||
405|501)
|
||||
# some APIs reject wrong method — try GET already failed; soft note
|
||||
fail "landing ${name} link" "HTTP $code · $u"
|
||||
;;
|
||||
*)
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ]; then
|
||||
fail "landing ${name} link" "HTTP $code · $u"
|
||||
else
|
||||
warn "landing ${name} link" "HTTP $code · $u"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done < "${pref}.own"
|
||||
fi
|
||||
|
||||
# External store / docs: soft
|
||||
if [ -f "${pref}.ext" ]; then
|
||||
while IFS= read -r u; do
|
||||
[ -n "$u" ] || continue
|
||||
check_external_soft "landing ${name} external" "$u"
|
||||
done < "${pref}.ext"
|
||||
fi
|
||||
}
|
||||
|
||||
check_one_landing "bank" "$BANK_PUBLIC"
|
||||
check_one_landing "merchant" "$MERCHANT_PUBLIC"
|
||||
check_one_landing "exchange" "$EXCHANGE_PUBLIC"
|
||||
|
||||
# Cross-links between the three landings (always on local stack)
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ]; then
|
||||
check_landing_asset "cross bank→merchant intro" "$MERCHANT_PUBLIC/intro/"
|
||||
check_landing_asset "cross bank→exchange intro" "$EXCHANGE_PUBLIC/intro/"
|
||||
check_landing_asset "cross merchant→bank intro" "$BANK_PUBLIC/intro/"
|
||||
check_landing_asset "cross exchange→bank intro" "$BANK_PUBLIC/intro/"
|
||||
fi
|
||||
|
||||
# Bank-only: shared-pool withdraw mint + static withdraw files + shop assets
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ] || [ -n "${BANK_PUBLIC:-}" ]; then
|
||||
check_landing_asset "bank shop-pay.js" "$BANK_PUBLIC/intro/shop-pay.js" 1
|
||||
check_landing_asset "bank shop-pay.css" "$BANK_PUBLIC/intro/shop-pay.css" 1
|
||||
dw_code=$(http_body "$BANK_PUBLIC/intro/demo-withdraw.json" "$tmp/dw.json")
|
||||
case "$dw_code" in
|
||||
200)
|
||||
if python3 - "$tmp/dw.json" <<'PY'
|
||||
import json, re, sys
|
||||
d = json.load(open(sys.argv[1]))
|
||||
if not d.get("ok", True) and "taler_withdraw_uri" not in d:
|
||||
print("not ok"); sys.exit(1)
|
||||
u = d.get("taler_withdraw_uri") or ""
|
||||
m = re.match(r"^taler://withdraw/([^/]+)/taler-integration/([0-9a-fA-F-]+)$", u)
|
||||
if not m:
|
||||
print("bad uri:", u[:120]); sys.exit(1)
|
||||
if ":" not in m.group(1):
|
||||
print("missing port:", m.group(1)); sys.exit(1)
|
||||
print(u[:88])
|
||||
sys.exit(0)
|
||||
PY
|
||||
then
|
||||
ok "bank /intro/demo-withdraw.json" "$(python3 -c 'import json;print(json.load(open("'"$tmp/dw.json"'")).get("taler_withdraw_uri","")[:80])' 2>/dev/null || true)"
|
||||
wid=$(python3 -c 'import json;print(json.load(open("'"$tmp/dw.json"'")).get("withdrawal_id",""))' 2>/dev/null || true)
|
||||
if [ -n "$wid" ]; then
|
||||
check_landing_asset "bank taler-integration withdraw op" \
|
||||
"$BANK_PUBLIC/taler-integration/withdrawal-operation/${wid}"
|
||||
fi
|
||||
else
|
||||
fail "bank /intro/demo-withdraw.json" "invalid taler://withdraw shape"
|
||||
fi
|
||||
;;
|
||||
405|501|404|502|503|000)
|
||||
fail "bank /intro/demo-withdraw.json" "HTTP $dw_code (want 200)"
|
||||
;;
|
||||
*)
|
||||
if [ "${LOCAL_STACK:-1}" = "1" ]; then
|
||||
fail "bank /intro/demo-withdraw.json" "HTTP $dw_code want 200"
|
||||
else
|
||||
warn "bank /intro/demo-withdraw.json" "HTTP $dw_code"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Merchant landing shop assets
|
||||
check_landing_asset "merchant shop-pay.js" "$MERCHANT_PUBLIC/intro/shop-pay.js" 1
|
||||
check_landing_asset "merchant shop-pay.css" "$MERCHANT_PUBLIC/intro/shop-pay.css" 1
|
||||
|
||||
summary
|
||||
469
scripts/taler-monitoring/check_versions.sh
Executable file
469
scripts/taler-monitoring/check_versions.sh
Executable file
|
|
@ -0,0 +1,469 @@
|
|||
#!/usr/bin/env bash
|
||||
# Area versions-### — Taler packages vs deb.taler.net (trixie) + repo availability.
|
||||
#
|
||||
# Checks:
|
||||
# 1) deb.taler.net apt endpoints reachable (InRelease / Packages / sample .deb)
|
||||
# 2) containers can reach deb.taler.net (install path from inside)
|
||||
# 3) installed taler*/libeufin*/libtaler*/libdonau* versions vs suite index
|
||||
#
|
||||
# Env:
|
||||
# TALER_APT_SUITE=trixie
|
||||
# TALER_APT_BASE=https://deb.taler.net/apt/debian
|
||||
# TALER_APT_INDEX=…/dists/trixie/main/binary-amd64/Packages
|
||||
# TALER_APT_TESTING_INDEX=…/dists/trixie-testing/…
|
||||
# TALER_PKG_BEHIND=warn|error (default: core packages ERROR if behind, else warn)
|
||||
# SKIP_SSH=1 skip container install checks
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
set_area versions
|
||||
|
||||
SUITE="${TALER_APT_SUITE:-trixie}"
|
||||
APT_BASE="${TALER_APT_BASE:-https://deb.taler.net/apt/debian}"
|
||||
APT_BASE="${APT_BASE%/}"
|
||||
INDEX="${TALER_APT_INDEX:-${APT_BASE}/dists/${SUITE}/main/binary-amd64/Packages}"
|
||||
INRELEASE="${TALER_APT_INRELEASE:-${APT_BASE}/dists/${SUITE}/InRelease}"
|
||||
TESTING_INDEX="${TALER_APT_TESTING_INDEX:-${APT_BASE}/dists/${SUITE}-testing/main/binary-amd64/Packages}"
|
||||
BEHIND_MODE="${TALER_PKG_BEHIND:-warn}"
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
http_get() {
|
||||
# usage: http_get URL [outfile] → prints http_code; body to outfile or /dev/null
|
||||
local url="$1" out="${2:-/dev/null}"
|
||||
curl -sS --max-time 30 -L -o "$out" -w '%{http_code}' "$url" 2>/dev/null || echo 000
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1) OUTSIDE — deb.taler.net install source (runner / laptop, no SSH)
|
||||
# ---------------------------------------------------------------------------
|
||||
section "versions · outside · deb.taler.net (${SUITE})"
|
||||
|
||||
# DNS
|
||||
HOST_APT="${APT_BASE#https://}"; HOST_APT="${HOST_APT#http://}"; HOST_APT="${HOST_APT%%/*}"
|
||||
if getent hosts "$HOST_APT" >/dev/null 2>&1 \
|
||||
|| python3 -c "import socket; socket.getaddrinfo('${HOST_APT}', 443)" >/dev/null 2>&1; then
|
||||
ip=$(python3 -c "import socket; print(socket.getaddrinfo('${HOST_APT}',443)[0][4][0])" 2>/dev/null || true)
|
||||
ok "outside DNS ${HOST_APT}" "${ip:-resolved}"
|
||||
else
|
||||
fail "outside DNS ${HOST_APT}" "unresolvable — cannot use as apt source"
|
||||
fi
|
||||
|
||||
# HTTPS portal / apt root
|
||||
code=$(http_get "https://${HOST_APT}/")
|
||||
[ "$code" = "200" ] && ok "outside https://${HOST_APT}/" "HTTP $code" \
|
||||
|| warn "outside https://${HOST_APT}/" "HTTP $code"
|
||||
|
||||
code=$(http_get "${APT_BASE}/")
|
||||
case "$code" in
|
||||
200|301|302) ok "outside apt base" "${APT_BASE}/ HTTP $code" ;;
|
||||
*) fail "outside apt base" "${APT_BASE}/ HTTP $code" ;;
|
||||
esac
|
||||
|
||||
# Suite metadata (what apt update needs)
|
||||
code=$(http_get "$INRELEASE" "$tmp/InRelease")
|
||||
if [ "$code" = "200" ] && [ -s "$tmp/InRelease" ]; then
|
||||
ok "outside InRelease" "${SUITE} HTTP 200 · $(wc -c <"$tmp/InRelease" | tr -d ' ') bytes"
|
||||
else
|
||||
fail "outside InRelease" "HTTP $code — $INRELEASE (apt update will fail)"
|
||||
fi
|
||||
|
||||
RELEASE_URL="${APT_BASE}/dists/${SUITE}/Release"
|
||||
code=$(http_get "$RELEASE_URL" "$tmp/Release")
|
||||
[ "$code" = "200" ] && ok "outside Release" "${SUITE} HTTP 200" \
|
||||
|| warn "outside Release" "HTTP $code — $RELEASE_URL"
|
||||
|
||||
# Packages (plain) — required for version compare
|
||||
code=$(http_get "$INDEX" "$tmp/Packages")
|
||||
if [ "$code" = "200" ] && [ -s "$tmp/Packages" ] && grep -q '^Package: ' "$tmp/Packages"; then
|
||||
n_pkg=$(grep -c '^Package: ' "$tmp/Packages" || true)
|
||||
ok "outside Packages" "${SUITE} HTTP 200 · ${n_pkg} packages"
|
||||
else
|
||||
fail "outside Packages" "HTTP $code — $INDEX"
|
||||
# still try more probes, but cannot compare versions without index
|
||||
fi
|
||||
|
||||
# Packages.gz (apt often prefers this)
|
||||
PKGZ_URL="${APT_BASE}/dists/${SUITE}/main/binary-amd64/Packages.gz"
|
||||
code=$(http_get "$PKGZ_URL" "$tmp/Packages.gz")
|
||||
if [ "$code" = "200" ] && [ -s "$tmp/Packages.gz" ]; then
|
||||
ok "outside Packages.gz" "${SUITE} HTTP 200 · $(wc -c <"$tmp/Packages.gz" | tr -d ' ') bytes"
|
||||
else
|
||||
warn "outside Packages.gz" "HTTP $code — apt may still use plain Packages"
|
||||
fi
|
||||
|
||||
# Sample pool .deb downloadable (proves packages are installable, not just listed)
|
||||
if [ -s "$tmp/Packages" ]; then
|
||||
DEB_PATH=$(awk '
|
||||
/^Package: taler-exchange$/ { p=1 }
|
||||
p && /^Filename: / { sub(/^Filename: /,""); print; exit }
|
||||
p && /^$/ { p=0 }
|
||||
' "$tmp/Packages")
|
||||
[ -z "$DEB_PATH" ] && DEB_PATH=$(awk '
|
||||
/^Package: taler-merchant$/ { p=1 }
|
||||
p && /^Filename: / { sub(/^Filename: /,""); print; exit }
|
||||
p && /^$/ { p=0 }
|
||||
' "$tmp/Packages")
|
||||
if [ -n "$DEB_PATH" ]; then
|
||||
DEB_URL="${APT_BASE}/${DEB_PATH}"
|
||||
code=$(curl -sS --max-time 30 -o /dev/null -w '%{http_code}' -r 0-128 "$DEB_URL" 2>/dev/null || echo 000)
|
||||
case "$code" in
|
||||
200|206)
|
||||
ok "outside pool .deb" "$(basename "$DEB_PATH") HTTP $code"
|
||||
;;
|
||||
*)
|
||||
fail "outside pool .deb" "HTTP $code — $DEB_URL (index ok but debs not fetchable)"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
warn "outside pool .deb" "no Filename for taler-exchange/merchant in index"
|
||||
fi
|
||||
|
||||
for pkg in taler-exchange taler-merchant libeufin-bank; do
|
||||
ver=$(awk -v p="$pkg" '
|
||||
$0=="Package: "p { hit=1; next }
|
||||
hit && /^Version: / { sub(/^Version: /,""); print; exit }
|
||||
hit && /^$/ { hit=0 }
|
||||
' "$tmp/Packages")
|
||||
if [ -n "$ver" ]; then
|
||||
ok "outside suite offers ${pkg}" "$ver"
|
||||
else
|
||||
fail "outside suite offers ${pkg}" "missing from ${SUITE} Packages"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
HAVE_TESTING=0
|
||||
code=$(http_get "$TESTING_INDEX" "$tmp/Packages-testing")
|
||||
if [ "$code" = "200" ] && [ -s "$tmp/Packages-testing" ] && grep -q '^Package: ' "$tmp/Packages-testing"; then
|
||||
HAVE_TESTING=1
|
||||
ok "outside ${SUITE}-testing Packages" "HTTP 200 (optional compare)"
|
||||
else
|
||||
info "outside ${SUITE}-testing Packages" "HTTP ${code:-000} (optional)"
|
||||
fi
|
||||
|
||||
# TLS: certificate verify (curl default) already used above; explicit openssl probe if available
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
if echo | openssl s_client -servername "$HOST_APT" -connect "${HOST_APT}:443" 2>/dev/null \
|
||||
| grep -q 'Verify return code: 0'; then
|
||||
ok "outside TLS ${HOST_APT}" "verify ok"
|
||||
else
|
||||
# curl succeeded with default CA — soft warn only
|
||||
warn "outside TLS ${HOST_APT}" "openssl verify not clean (curl may still work)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Need Packages index for later compare
|
||||
if [ ! -s "$tmp/Packages" ] || ! grep -q '^Package: ' "$tmp/Packages"; then
|
||||
fail "outside Packages usable" "cannot continue version compare without suite index"
|
||||
summary
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2) INSIDE — containers can reach deb.taler.net (pasta / install path)
|
||||
# ---------------------------------------------------------------------------
|
||||
section "versions · inside · containers → deb.taler.net"
|
||||
|
||||
if [ "${SKIP_SSH}" = "1" ]; then
|
||||
warn "ssh" "SKIP_SSH=1 — skip container reachability + installed package compare"
|
||||
info "outside-only" "deb.taler.net public checks completed above"
|
||||
summary
|
||||
exit 0
|
||||
fi
|
||||
if ! koopa_ssh_ok; then
|
||||
err "ssh" "cannot reach ${KOOPA_SSH} — set SKIP_SSH=1 to skip container checks"
|
||||
info "outside-only" "deb.taler.net public checks completed above"
|
||||
summary
|
||||
exit 1
|
||||
fi
|
||||
ok "ssh ${KOOPA_SSH}"
|
||||
|
||||
INRELEASE_URL="${APT_BASE}/dists/${SUITE}/InRelease"
|
||||
# Write remote script to a file (avoids bash 3.2 parse bugs with case/;; inside $(…)<<heredoc).
|
||||
{
|
||||
printf 'INRELEASE_URL=%q\n' "$INRELEASE_URL"
|
||||
cat <<'REMOTE'
|
||||
set +e
|
||||
resolve_ctr() {
|
||||
local want="$1" c
|
||||
c=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -Fx "$want" | head -1)
|
||||
if [ -n "$c" ]; then echo "$c"; return; fi
|
||||
case "$want" in
|
||||
*exchange*) podman ps --format '{{.Names}}' 2>/dev/null | grep -i exchange | head -1 ;;
|
||||
*bank*) podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'hacktivism-bank|taler-bank' | head -1 ;;
|
||||
*)
|
||||
c=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -E '^taler-hacktivism$' | head -1)
|
||||
if [ -n "$c" ]; then echo "$c"; return; fi
|
||||
podman ps --format '{{.Names}}' 2>/dev/null | grep -iE 'merchant|hacktivism' | grep -viE 'bank|exchange' | head -1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
for want in taler-hacktivism-exchange-ansible taler-hacktivism-bank taler-hacktivism; do
|
||||
c=$(resolve_ctr "$want")
|
||||
[ -n "$c" ] || continue
|
||||
role=other
|
||||
case "$c" in
|
||||
*exchange*) role=exchange ;;
|
||||
*bank*) role=bank ;;
|
||||
*) role=merchant ;;
|
||||
esac
|
||||
code=$(podman exec "$c" curl -sS -m 12 -o /dev/null -w '%{http_code}' "$INRELEASE_URL" 2>/dev/null || echo 000)
|
||||
echo "R|${role}|${c}|${code}"
|
||||
if podman exec "$c" bash -lc 'grep -Rqs deb.taler.net /etc/apt/sources.list /etc/apt/sources.list.d 2>/dev/null'; then
|
||||
echo "S|${role}|${c}|yes"
|
||||
else
|
||||
echo "S|${role}|${c}|no"
|
||||
fi
|
||||
podman exec "$c" dpkg-query -W -f '${Package}\t${Version}\n' 2>/dev/null \
|
||||
| awk -v role="$role" -v ctr="$c" '
|
||||
$1 ~ /^(taler-|libeufin-|libtaler|libdonau)/ {
|
||||
printf "P|%s|%s|%s|%s\n", role, ctr, $1, $2
|
||||
}'
|
||||
done
|
||||
REMOTE
|
||||
} >"$tmp/remote-versions.sh"
|
||||
|
||||
REMOTE_OUT=$(koopa_ssh_bash 60 <"$tmp/remote-versions.sh" || true)
|
||||
|
||||
printf '%s\n' "$REMOTE_OUT" | grep -E '^R\|' >"$tmp/reach.tsv" || true
|
||||
printf '%s\n' "$REMOTE_OUT" | grep -E '^S\|' >"$tmp/sources.tsv" || true
|
||||
printf '%s\n' "$REMOTE_OUT" | grep -E '^P\|' >"$tmp/installed.tsv" || true
|
||||
|
||||
if [ -s "$tmp/reach.tsv" ]; then
|
||||
while IFS='|' read -r _ role ctr code; do
|
||||
if [ "$code" = "200" ]; then
|
||||
ok "container ${role} → deb.taler.net" "${ctr} InRelease HTTP $code"
|
||||
else
|
||||
fail "container ${role} → deb.taler.net" "${ctr} InRelease HTTP $code — apt install/update will fail"
|
||||
fi
|
||||
done <"$tmp/reach.tsv"
|
||||
else
|
||||
warn "container → deb.taler.net" "no reachability rows"
|
||||
fi
|
||||
|
||||
if [ -s "$tmp/sources.tsv" ]; then
|
||||
while IFS='|' read -r _ role ctr has; do
|
||||
if [ "$has" = "yes" ]; then
|
||||
ok "container ${role} apt source" "${ctr} lists deb.taler.net"
|
||||
else
|
||||
warn "container ${role} apt source" "${ctr} no deb.taler.net in sources.list*"
|
||||
fi
|
||||
done <"$tmp/sources.tsv"
|
||||
fi
|
||||
|
||||
if [ ! -s "$tmp/installed.tsv" ]; then
|
||||
fail "installed packages" "none found in taler containers"
|
||||
summary
|
||||
exit 1
|
||||
fi
|
||||
n_inst=$(wc -l <"$tmp/installed.tsv" | tr -d ' ')
|
||||
ok "collected installed packages" "${n_inst} rows"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3) compare installed vs trixie (and note testing)
|
||||
# ---------------------------------------------------------------------------
|
||||
section "versions · compare installed vs ${SUITE}"
|
||||
|
||||
export BEHIND_MODE HAVE_TESTING
|
||||
export TALER_APT_SUITE="$SUITE"
|
||||
CORE_PKGS="taler-exchange taler-exchange-database taler-merchant taler-merchant-webui libeufin-bank libeufin-common libtalerexchange libtalermerchant taler-terms-generator"
|
||||
export CORE_PKGS
|
||||
|
||||
python3 - "$tmp/Packages" "$tmp/Packages-testing" "$tmp/installed.tsv" <<'PY' >"$tmp/py.out"
|
||||
import sys, os
|
||||
from collections import defaultdict
|
||||
|
||||
main_idx, test_idx, inst_path = sys.argv[1:4]
|
||||
have_testing = os.environ.get("HAVE_TESTING", "0") == "1"
|
||||
core = set(os.environ.get("CORE_PKGS", "").split())
|
||||
suite = os.environ.get("TALER_APT_SUITE", "trixie")
|
||||
|
||||
def _ord_ch(c):
|
||||
if c == "~":
|
||||
return -1
|
||||
if c.isdigit():
|
||||
return 0
|
||||
if c.isalpha():
|
||||
return ord(c)
|
||||
return ord(c) + 256
|
||||
|
||||
def _split_ver(v):
|
||||
epoch = 0
|
||||
if ":" in v:
|
||||
e, v = v.split(":", 1)
|
||||
try:
|
||||
epoch = int(e)
|
||||
except ValueError:
|
||||
epoch = 0
|
||||
if "-" in v:
|
||||
upstream, deb = v.rsplit("-", 1)
|
||||
else:
|
||||
upstream, deb = v, ""
|
||||
return epoch, upstream, deb
|
||||
|
||||
def _cmp_part(a, b):
|
||||
ia = ib = 0
|
||||
while ia < len(a) or ib < len(b):
|
||||
while True:
|
||||
ca = a[ia] if ia < len(a) and not a[ia].isdigit() else ""
|
||||
cb = b[ib] if ib < len(b) and not b[ib].isdigit() else ""
|
||||
if ca == "" and cb == "":
|
||||
break
|
||||
if ca == "" and cb:
|
||||
return -1 if _ord_ch(cb) > 0 else (1 if cb == "~" else -1)
|
||||
if cb == "" and ca:
|
||||
return 1 if _ord_ch(ca) > 0 else (-1 if ca == "~" else 1)
|
||||
oa, ob = _ord_ch(ca), _ord_ch(cb)
|
||||
ia += 1
|
||||
ib += 1
|
||||
if oa != ob:
|
||||
return (oa > ob) - (oa < ob)
|
||||
sa = sb = ""
|
||||
while ia < len(a) and a[ia].isdigit():
|
||||
sa += a[ia]; ia += 1
|
||||
while ib < len(b) and b[ib].isdigit():
|
||||
sb += b[ib]; ib += 1
|
||||
na = int(sa) if sa else 0
|
||||
nb = int(sb) if sb else 0
|
||||
if na != nb:
|
||||
return (na > nb) - (na < nb)
|
||||
return 0
|
||||
|
||||
def deb_cmp(a, b):
|
||||
if a == b:
|
||||
return 0
|
||||
ea, ua, da = _split_ver(a)
|
||||
eb, ub, db = _split_ver(b)
|
||||
if ea != eb:
|
||||
return (ea > eb) - (ea < eb)
|
||||
c = _cmp_part(ua, ub)
|
||||
if c:
|
||||
return c
|
||||
return _cmp_part(da, db)
|
||||
|
||||
def parse_packages(path):
|
||||
pkgs = {}
|
||||
if not path or not os.path.isfile(path) or os.path.getsize(path) == 0:
|
||||
return pkgs
|
||||
cur = ver = None
|
||||
with open(path, encoding="utf-8", errors="replace") as f:
|
||||
for line in f:
|
||||
line = line.rstrip("\n")
|
||||
if line.startswith("Package: "):
|
||||
if cur and ver and (cur not in pkgs or deb_cmp(ver, pkgs[cur]) > 0):
|
||||
pkgs[cur] = ver
|
||||
cur = line[9:].strip()
|
||||
ver = None
|
||||
elif line.startswith("Version: ") and cur:
|
||||
ver = line[9:].strip()
|
||||
elif line == "" and cur:
|
||||
if ver and (cur not in pkgs or deb_cmp(ver, pkgs[cur]) > 0):
|
||||
pkgs[cur] = ver
|
||||
cur = ver = None
|
||||
if cur and ver and (cur not in pkgs or deb_cmp(ver, pkgs[cur]) > 0):
|
||||
pkgs[cur] = ver
|
||||
return pkgs
|
||||
|
||||
main = parse_packages(main_idx)
|
||||
testing = parse_packages(test_idx) if have_testing else {}
|
||||
|
||||
by_pkg = defaultdict(list)
|
||||
with open(inst_path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line.startswith("P|"):
|
||||
continue
|
||||
parts = line.split("|")
|
||||
if len(parts) < 5:
|
||||
continue
|
||||
_, role, ctr, pkg, ver = parts[:5]
|
||||
by_pkg[pkg].append((role, ctr, ver))
|
||||
|
||||
for pkg in sorted(by_pkg.keys()):
|
||||
versions = sorted({v for _, _, v in by_pkg[pkg]}, key=lambda v: v)
|
||||
# pick "highest" via deb_cmp
|
||||
inst = versions[0]
|
||||
for v in versions[1:]:
|
||||
if deb_cmp(v, inst) > 0:
|
||||
inst = v
|
||||
roles = ",".join(sorted({r for r, _, _ in by_pkg[pkg]}))
|
||||
multi = len(set(versions)) > 1
|
||||
trixie = main.get(pkg)
|
||||
testv = testing.get(pkg)
|
||||
status = "ok"
|
||||
detail = ""
|
||||
if multi:
|
||||
status = "warn"
|
||||
detail = "multiple installed: " + ", ".join(sorted(set(versions)))
|
||||
if trixie is None:
|
||||
if status == "ok":
|
||||
status = "info"
|
||||
detail = (detail + "; " if detail else "") + f"not in {suite} main index"
|
||||
else:
|
||||
c = deb_cmp(inst, trixie)
|
||||
if c == 0:
|
||||
detail = (detail + "; " if detail else "") + f"= {suite} {trixie}"
|
||||
elif c < 0:
|
||||
status = "behind"
|
||||
detail = f"installed {inst} < {suite} {trixie}"
|
||||
else:
|
||||
status = "ahead"
|
||||
detail = f"installed {inst} > {suite} {trixie}"
|
||||
if testv and deb_cmp(inst, testv) == 0:
|
||||
detail += f" (={suite}-testing)"
|
||||
elif testv:
|
||||
detail += f" (testing has {testv})"
|
||||
core_flag = "1" if pkg in core else "0"
|
||||
print(f"{status}|{pkg}|{inst}|{trixie or '-'}|{roles}|{core_flag}|{detail}")
|
||||
PY
|
||||
|
||||
ok_n=0; ahead_n=0; behind_n=0
|
||||
while IFS='|' read -r status pkg inst trixie roles core detail; do
|
||||
[ -n "${status:-}" ] || continue
|
||||
label="pkg ${pkg} (${roles})"
|
||||
case "$status" in
|
||||
ok)
|
||||
ok "$label" "${inst}"
|
||||
ok_n=$((ok_n + 1))
|
||||
;;
|
||||
ahead)
|
||||
info "$label" "$detail"
|
||||
ahead_n=$((ahead_n + 1))
|
||||
;;
|
||||
behind)
|
||||
behind_n=$((behind_n + 1))
|
||||
if [ "$core" = "1" ] || [ "$BEHIND_MODE" = "error" ]; then
|
||||
fail "$label" "$detail"
|
||||
else
|
||||
warn "$label" "$detail"
|
||||
fi
|
||||
;;
|
||||
warn)
|
||||
warn "$label" "$detail"
|
||||
;;
|
||||
info)
|
||||
info "$label" "installed ${inst}${detail:+ — $detail}"
|
||||
;;
|
||||
*)
|
||||
info "$label" "$status $detail"
|
||||
;;
|
||||
esac
|
||||
done <"$tmp/py.out"
|
||||
|
||||
# Core packages must exist somewhere
|
||||
for need in taler-exchange libeufin-bank taler-merchant; do
|
||||
if grep -qE "^P\|[^|]+\|[^|]+\|${need}\|" "$tmp/installed.tsv"; then
|
||||
ok "core installed ${need}"
|
||||
else
|
||||
fail "core installed ${need}" "not present in any taler container"
|
||||
fi
|
||||
done
|
||||
|
||||
info "suite" "deb.taler.net ${SUITE} (testing_index=${HAVE_TESTING})"
|
||||
info "tally" "match=${ok_n} ahead=${ahead_n} behind=${behind_n}"
|
||||
|
||||
summary
|
||||
7
scripts/taler-monitoring/harness-live.txt
Normal file
7
scripts/taler-monitoring/harness-live.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Optional: taler-harness against *our* exchange (needs a harness version
|
||||
# that accepts current /config JSON). Older installs often FAIL codec checks
|
||||
# even when the exchange is fine — trust `./taler-monitoring.sh urls` first.
|
||||
#
|
||||
# id<TAB>command
|
||||
|
||||
lint-exchange-goa taler-harness deployment lint-exchange-url https://exchange.hacktivism.ch/
|
||||
41
scripts/taler-monitoring/harness-tests.txt
Normal file
41
scripts/taler-monitoring/harness-tests.txt
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Real-life–relevant taler-harness integration tests
|
||||
# (from: taler-harness list-integrationtests)
|
||||
#
|
||||
# Criteria: everyday user/merchant/bank paths — withdraw, pay, deposit, refund,
|
||||
# libeufin bank, templates/paywall. Excludes KYC edge cases, experimental,
|
||||
# timetravel, perf, backup, mailbox, most fault-injection.
|
||||
#
|
||||
# These spin up a local TESTKUDOS stack (same client code paths as production).
|
||||
# For tests against *public* demos, see harness-live.txt / `./taler-monitoring.sh live`.
|
||||
|
||||
# --- Core path: get money + spend it (the GOA story) ---
|
||||
simple-payment
|
||||
withdrawal-bank-integrated
|
||||
payment
|
||||
|
||||
# --- Withdraw variants users actually hit ---
|
||||
withdrawal-manual
|
||||
withdrawal-external
|
||||
withdrawal-idempotent
|
||||
|
||||
# --- Pay variants shops use ---
|
||||
payment-template
|
||||
paywall-flow
|
||||
payment-idempotency
|
||||
payment-zero
|
||||
payment-abort
|
||||
otp
|
||||
|
||||
# --- After pay: coins / merchant ops ---
|
||||
deposit
|
||||
refund
|
||||
refund-auto
|
||||
wallet-refresh
|
||||
|
||||
# --- Bank stack we run (libeufin / regional) ---
|
||||
libeufin-bank
|
||||
bank-api
|
||||
|
||||
# --- Client hygiene ---
|
||||
wallet-config
|
||||
term-of-service-format
|
||||
536
scripts/taler-monitoring/lib.sh
Executable file
536
scripts/taler-monitoring/lib.sh
Executable file
|
|
@ -0,0 +1,536 @@
|
|||
# shellcheck shell=bash
|
||||
# Shared helpers for taler-monitoring (laptop or koopa).
|
||||
|
||||
# Default stack = GOA / hacktivism (overridden by TALER_DOMAIN / --domain)
|
||||
: "${TALER_DOMAIN:=hacktivism.ch}"
|
||||
: "${BANK_PUBLIC:=https://bank.hacktivism.ch}"
|
||||
: "${EXCHANGE_PUBLIC:=https://exchange.hacktivism.ch}"
|
||||
: "${MERCHANT_PUBLIC:=https://taler.hacktivism.ch}"
|
||||
: "${BANK_LOCAL:=http://127.0.0.1:9012}"
|
||||
: "${EXCHANGE_LOCAL:=http://127.0.0.1:9011}"
|
||||
: "${MERCHANT_LOCAL:=https://127.0.0.1:9010}"
|
||||
: "${LANDING_LOCAL:=http://127.0.0.1:9013}"
|
||||
: "${KOOPA_SSH:=koopa}"
|
||||
: "${MERCHANT_INSTANCE:=goa-demo-cp4zqk}"
|
||||
: "${WITHDRAW_AMT:=GOA:20}" # single-shot fallback; e2e ladder uses ATM notes
|
||||
: "${PAY_AMT:=GOA:0.01}"
|
||||
: "${CREDIT_AMT:=GOA:400}" # covers ATM ladder 20+50+100+200
|
||||
: "${TIMEOUT:=12}"
|
||||
: "${E2E_TIMEOUT:=55}" # whole e2e budget; skip rest when exceeded
|
||||
: "${E2E_PAY_SECS:=22}" # dedicated seconds for pay handle-uri (avoid Alarm clock)
|
||||
# Devtest: inject reserve credit via wire-gateway admin/add-incoming (optional).
|
||||
# Default off once wirewatch DNS works; set E2E_FAKE_INCOMING=1 to force.
|
||||
: "${E2E_FAKE_INCOMING:=0}"
|
||||
# SSH must never hang the monitoring run
|
||||
: "${SSH_CONNECT_TIMEOUT:=3}"
|
||||
: "${SSH_CMD_TIMEOUT:=12}" # hard cap for whole remote script (seconds)
|
||||
: "${SKIP_SSH:=0}"
|
||||
# Expected currency for public /config checks (empty = report only, don't fail)
|
||||
: "${EXPECT_CURRENCY:=GOA}"
|
||||
# 1 = this is the local koopa/hacktivism stack (inside/e2e/SSH make sense)
|
||||
: "${LOCAL_STACK:=1}"
|
||||
# Probe merchant host candidates when applying a generic domain (0=off)
|
||||
: "${TALER_DOMAIN_PROBE:=1}"
|
||||
|
||||
BANK_PUBLIC=${BANK_PUBLIC%/}
|
||||
EXCHANGE_PUBLIC=${EXCHANGE_PUBLIC%/}
|
||||
MERCHANT_PUBLIC=${MERCHANT_PUBLIC%/}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Domain presets → public bank / exchange / merchant base URLs
|
||||
#
|
||||
# TALER_DOMAIN=hacktivism.ch (default, GOA, local stack)
|
||||
# TALER_DOMAIN=taler.net → demo.taler.net (KUDOS)
|
||||
# TALER_DOMAIN=demo.taler.net
|
||||
# TALER_DOMAIN=taler-ops.ch → exchange.taler-ops.ch (CHF; bank/merchant if up)
|
||||
# TALER_DOMAIN=example.org → bank/exchange/backend|taler|merchant.example.org
|
||||
#
|
||||
# Explicit BANK_PUBLIC / EXCHANGE_PUBLIC / MERCHANT_PUBLIC still win if set
|
||||
# *after* apply_taler_domain, or pass full URLs via --bank/--exchange/--merchant.
|
||||
# ---------------------------------------------------------------------------
|
||||
_normalize_domain() {
|
||||
local d="$1"
|
||||
d="${d#https://}"
|
||||
d="${d#http://}"
|
||||
d="${d%%/*}"
|
||||
d="${d%%:*}"
|
||||
# Strip service host prefix only if a real domain remains (has a dot).
|
||||
# e.g. bank.demo.taler.net → demo.taler.net, taler.hacktivism.ch → hacktivism.ch
|
||||
# but NOT taler.net → net
|
||||
case "$d" in
|
||||
bank.*|exchange.*|taler.*|backend.*|merchant.*|shop.*|libeufin.*)
|
||||
rest="${d#*.}"
|
||||
if [[ "$rest" == *.* ]]; then
|
||||
d="$rest"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
printf '%s' "$d"
|
||||
}
|
||||
|
||||
_probe_https_config() {
|
||||
# 0 if https://$1/config returns 200
|
||||
local host="$1" code
|
||||
code=$(curl -skS --max-redirs 0 -m 4 -o /dev/null -w '%{http_code}' "https://${host}/config" 2>/dev/null || echo 000)
|
||||
[ "$code" = "200" ]
|
||||
}
|
||||
|
||||
apply_taler_domain() {
|
||||
local raw="${1:-}"
|
||||
local d
|
||||
[ -n "$raw" ] || return 0
|
||||
d=$(_normalize_domain "$raw")
|
||||
TALER_DOMAIN="$d"
|
||||
|
||||
case "$d" in
|
||||
# Local koopa stack only — SSH / inside / e2e allowed
|
||||
koopa|hacktivism.ch|hacktivism)
|
||||
BANK_PUBLIC="https://bank.hacktivism.ch"
|
||||
EXCHANGE_PUBLIC="https://exchange.hacktivism.ch"
|
||||
MERCHANT_PUBLIC="https://taler.hacktivism.ch"
|
||||
EXPECT_CURRENCY="GOA"
|
||||
LOCAL_STACK=1
|
||||
SKIP_SSH=0
|
||||
TALER_DOMAIN="hacktivism.ch"
|
||||
: "${WITHDRAW_AMT:=GOA:20}"
|
||||
: "${PAY_AMT:=GOA:0.01}"
|
||||
: "${CREDIT_AMT:=GOA:400}"
|
||||
;;
|
||||
taler.net|demo.taler.net)
|
||||
# Official public demo (KUDOS) — public only, never SSH; tiny e2e amounts
|
||||
BANK_PUBLIC="https://bank.demo.taler.net"
|
||||
EXCHANGE_PUBLIC="https://exchange.demo.taler.net"
|
||||
MERCHANT_PUBLIC="https://backend.demo.taler.net"
|
||||
EXPECT_CURRENCY="KUDOS"
|
||||
LOCAL_STACK=0
|
||||
SKIP_SSH=1
|
||||
MERCHANT_INSTANCE="${MERCHANT_INSTANCE:-sandbox}"
|
||||
WITHDRAW_AMT="${WITHDRAW_AMT:-KUDOS:20}"
|
||||
PAY_AMT="${PAY_AMT:-KUDOS:0.01}"
|
||||
CREDIT_AMT="${CREDIT_AMT:-KUDOS:100}"
|
||||
TALER_DOMAIN="demo.taler.net"
|
||||
;;
|
||||
taler-ops.ch)
|
||||
# Public CHF exchange; bank/merchant hosts vary — probe common names
|
||||
EXCHANGE_PUBLIC="https://exchange.taler-ops.ch"
|
||||
BANK_PUBLIC="https://bank.taler-ops.ch"
|
||||
MERCHANT_PUBLIC="https://backend.taler-ops.ch"
|
||||
EXPECT_CURRENCY="CHF"
|
||||
LOCAL_STACK=0
|
||||
SKIP_SSH=1
|
||||
WITHDRAW_AMT="${WITHDRAW_AMT:-CHF:20}"
|
||||
PAY_AMT="${PAY_AMT:-CHF:0.01}"
|
||||
CREDIT_AMT="${CREDIT_AMT:-CHF:100}"
|
||||
if [ "${TALER_DOMAIN_PROBE}" = "1" ]; then
|
||||
local h
|
||||
for h in bank.taler-ops.ch bank.demo.taler-ops.ch; do
|
||||
_probe_https_config "$h" && { BANK_PUBLIC="https://$h"; break; }
|
||||
done
|
||||
for h in backend.taler-ops.ch merchant.taler-ops.ch taler.taler-ops.ch shop.taler-ops.ch; do
|
||||
_probe_https_config "$h" && { MERCHANT_PUBLIC="https://$h"; break; }
|
||||
done
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Any other domain — public HTTPS only, never SSH to koopa
|
||||
BANK_PUBLIC="https://bank.${d}"
|
||||
EXCHANGE_PUBLIC="https://exchange.${d}"
|
||||
MERCHANT_PUBLIC="https://backend.${d}"
|
||||
EXPECT_CURRENCY="${EXPECT_CURRENCY:-}" # unknown — don't hard-fail currency
|
||||
LOCAL_STACK=0
|
||||
SKIP_SSH=1
|
||||
if [ "${TALER_DOMAIN_PROBE}" = "1" ]; then
|
||||
local h
|
||||
for h in "backend.${d}" "taler.${d}" "merchant.${d}" "shop.${d}"; do
|
||||
_probe_https_config "$h" && { MERCHANT_PUBLIC="https://$h"; break; }
|
||||
done
|
||||
for h in "bank.${d}" "libeufin.${d}"; do
|
||||
_probe_https_config "$h" && { BANK_PUBLIC="https://$h"; break; }
|
||||
done
|
||||
_probe_https_config "exchange.${d}" || true
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
BANK_PUBLIC=${BANK_PUBLIC%/}
|
||||
EXCHANGE_PUBLIC=${EXCHANGE_PUBLIC%/}
|
||||
MERCHANT_PUBLIC=${MERCHANT_PUBLIC%/}
|
||||
|
||||
# Hard rule: only the local koopa/hacktivism stack may use SSH
|
||||
if [ "${LOCAL_STACK}" != "1" ]; then
|
||||
SKIP_SSH=1
|
||||
fi
|
||||
}
|
||||
|
||||
# Apply TALER_DOMAIN from env once (CLI exports TALER_DOMAIN_APPLIED=1 after overrides).
|
||||
if [ "${TALER_DOMAIN_APPLIED:-0}" != "1" ] \
|
||||
&& [ -n "${TALER_DOMAIN:-}" ] && [ "${TALER_DOMAIN}" != "hacktivism.ch" ]; then
|
||||
apply_taler_domain "$TALER_DOMAIN"
|
||||
TALER_DOMAIN_APPLIED=1
|
||||
fi
|
||||
|
||||
# Safe SSH: publickey only, short connect, overall alarm so we never block forever.
|
||||
SSH_BASE_OPTS=(
|
||||
-o BatchMode=yes
|
||||
-o ConnectTimeout="${SSH_CONNECT_TIMEOUT}"
|
||||
-o ConnectionAttempts=1
|
||||
-o ServerAliveInterval=3
|
||||
-o ServerAliveCountMax=2
|
||||
-o StrictHostKeyChecking=accept-new
|
||||
-o PreferredAuthentications=publickey
|
||||
-o PasswordAuthentication=no
|
||||
-o KbdInteractiveAuthentication=no
|
||||
-o GSSAPIAuthentication=no
|
||||
-o NumberOfPasswordPrompts=0
|
||||
)
|
||||
|
||||
# Hard wall-clock timeout so ssh/curl never block the monitoring run forever.
|
||||
with_timeout() {
|
||||
local secs="$1"; shift
|
||||
if command -v gtimeout >/dev/null 2>&1; then
|
||||
gtimeout --kill-after=2 "$secs" "$@"
|
||||
return $?
|
||||
fi
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout -k 2 "$secs" "$@" 2>/dev/null || timeout --kill-after=2 "$secs" "$@"
|
||||
return $?
|
||||
fi
|
||||
# Portable: perl alarm + process group kill
|
||||
perl -e '
|
||||
use strict; use warnings;
|
||||
my $secs = shift @ARGV;
|
||||
my $pid = fork();
|
||||
die "fork: $!" unless defined $pid;
|
||||
if ($pid == 0) {
|
||||
setpgrp(0, 0);
|
||||
exec @ARGV;
|
||||
exit 127;
|
||||
}
|
||||
$SIG{ALRM} = sub {
|
||||
kill "TERM", -$pid;
|
||||
select(undef, undef, undef, 1.0);
|
||||
kill "KILL", -$pid;
|
||||
exit 124;
|
||||
};
|
||||
alarm $secs;
|
||||
waitpid($pid, 0);
|
||||
my $code = $? >> 8;
|
||||
alarm 0;
|
||||
exit $code;
|
||||
' "$secs" "$@"
|
||||
}
|
||||
|
||||
# Probe: 0 if koopa SSH works quickly
|
||||
koopa_ssh_ok() {
|
||||
[ "${SKIP_SSH}" = "1" ] && return 1
|
||||
with_timeout $((SSH_CONNECT_TIMEOUT + 3)) \
|
||||
ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" 'echo ok' >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Run remote bash -s with optional stdin script; hard-capped
|
||||
# usage: koopa_ssh_bash [timeout_secs] <<'EOF' ... EOF
|
||||
# or: koopa_ssh_run timeout_secs 'remote command'
|
||||
koopa_ssh_run() {
|
||||
local t="${1:-$SSH_CMD_TIMEOUT}"
|
||||
shift
|
||||
with_timeout "$t" ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" "$@"
|
||||
}
|
||||
|
||||
koopa_ssh_bash() {
|
||||
local t="${1:-$SSH_CMD_TIMEOUT}"
|
||||
with_timeout "$t" ssh "${SSH_BASE_OPTS[@]}" "${KOOPA_SSH}" 'bash -s'
|
||||
}
|
||||
|
||||
if [ "${NO_COLOR:-0}" = "1" ] || [ ! -t 1 ]; then
|
||||
G= R= Y= C= N= B=
|
||||
else
|
||||
G=$'\e[32m'; R=$'\e[31m'; Y=$'\e[33m'; C=$'\e[36m'; B=$'\e[1m'; N=$'\e[0m'
|
||||
fi
|
||||
|
||||
PASS_N=0
|
||||
FAIL_N=0
|
||||
WARN_N=0
|
||||
INFO_N=0
|
||||
BLOCKERS=() # human-readable payment/withdraw blockers
|
||||
ERRORS=() # all ERROR lines (component scope)
|
||||
|
||||
# Test IDs by area: www-001, e2e-001, inside-001, …
|
||||
# Usage: set_area www then each ok/fail/warn/info/blocker/err auto-numbers.
|
||||
TEST_AREA=""
|
||||
TEST_N=0
|
||||
# Last issued id (www-001); set by _take_tid — not via $(…) so TEST_N persists.
|
||||
LAST_TID=""
|
||||
set_area() {
|
||||
TEST_AREA="$1"
|
||||
TEST_N=0
|
||||
LAST_TID=""
|
||||
}
|
||||
# Assign next id into LAST_TID (must not run in a subshell).
|
||||
_take_tid() {
|
||||
LAST_TID=""
|
||||
if [ -z "${TEST_AREA:-}" ]; then
|
||||
return
|
||||
fi
|
||||
TEST_N=$((TEST_N + 1))
|
||||
LAST_TID=$(printf '%s-%03d' "$TEST_AREA" "$TEST_N")
|
||||
}
|
||||
_fmt_tid() {
|
||||
# prefix "www-001 " or empty
|
||||
if [ -n "${LAST_TID:-}" ]; then
|
||||
printf '%s ' "$LAST_TID"
|
||||
fi
|
||||
}
|
||||
|
||||
ok() {
|
||||
local label="$1"
|
||||
_take_tid
|
||||
printf '%s[OK]%s %s%s\n' "$G" "$N" "$(_fmt_tid)" "$label"
|
||||
PASS_N=$((PASS_N + 1))
|
||||
}
|
||||
# component-scoped error: err bank "libeufin down" "detail"
|
||||
err() {
|
||||
local comp="$1" msg="$2" detail="${3:-}"
|
||||
_take_tid
|
||||
printf '%s[ERROR]%s %s%s: %s%s\n' "$R" "$N" "$(_fmt_tid)" "$comp" "$msg" "${detail:+ — $detail}"
|
||||
FAIL_N=$((FAIL_N + 1))
|
||||
ERRORS+=("${LAST_TID:+$LAST_TID }[$comp] $msg${detail:+ — $detail}")
|
||||
}
|
||||
# legacy fail label ...
|
||||
fail() {
|
||||
local label="$1" detail="${2:-}"
|
||||
_take_tid
|
||||
printf '%s[ERROR]%s %s%s%s\n' "$R" "$N" "$(_fmt_tid)" "$label" "${detail:+ — $detail}"
|
||||
FAIL_N=$((FAIL_N + 1))
|
||||
ERRORS+=("${LAST_TID:+$LAST_TID }$label${detail:+ — $detail}")
|
||||
}
|
||||
warn() {
|
||||
local label="$1" detail="${2:-}"
|
||||
_take_tid
|
||||
printf '%s[WARN]%s %s%s%s\n' "$Y" "$N" "$(_fmt_tid)" "$label" "${detail:+ — $detail}"
|
||||
WARN_N=$((WARN_N + 1))
|
||||
}
|
||||
info() {
|
||||
local label="$1" detail="${2:-}"
|
||||
_take_tid
|
||||
if [ -n "$detail" ]; then
|
||||
printf '%s[INFO]%s %s%s — %s\n' "$C" "$N" "$(_fmt_tid)" "$label" "$detail"
|
||||
else
|
||||
printf '%s[INFO]%s %s%s\n' "$C" "$N" "$(_fmt_tid)" "$label"
|
||||
fi
|
||||
INFO_N=$((INFO_N + 1))
|
||||
}
|
||||
blocker() {
|
||||
# Payment/withdraw path cannot proceed because of this
|
||||
local step="$1" msg="$2"
|
||||
_take_tid
|
||||
printf '%s[BLOCKER]%s %s%s: %s\n' "$R$B" "$N" "$(_fmt_tid)" "$step" "$msg"
|
||||
BLOCKERS+=("${LAST_TID:+$LAST_TID }[$step] $msg")
|
||||
FAIL_N=$((FAIL_N + 1))
|
||||
ERRORS+=("BLOCKER ${LAST_TID:+$LAST_TID }[$step] $msg")
|
||||
}
|
||||
section() { printf '\n%s== %s ==%s\n' "$B" "$*" "$N"; }
|
||||
|
||||
summary() {
|
||||
echo ""
|
||||
if [ "${#BLOCKERS[@]}" -gt 0 ]; then
|
||||
printf '%s--- BLOCKERS (fix/withdraw path) ---%s\n' "$R$B" "$N"
|
||||
local b
|
||||
for b in "${BLOCKERS[@]}"; do
|
||||
printf '%s • %s%s\n' "$R" "$b" "$N"
|
||||
done
|
||||
fi
|
||||
if [ "${#ERRORS[@]}" -gt 0 ] && [ "${#BLOCKERS[@]}" -lt "${#ERRORS[@]}" ]; then
|
||||
printf '%s--- ERRORS ---%s\n' "$R" "$N"
|
||||
local e
|
||||
for e in "${ERRORS[@]}"; do
|
||||
case "$e" in BLOCKER*) continue ;; esac
|
||||
printf '%s • %s%s\n' "$R" "$e" "$N"
|
||||
done
|
||||
fi
|
||||
printf 'totals: %s%d OK%s' "$G" "$PASS_N" "$N"
|
||||
[ "$FAIL_N" -gt 0 ] && printf ', %s%d ERROR%s' "$R" "$FAIL_N" "$N"
|
||||
[ "$WARN_N" -gt 0 ] && printf ', %s%d WARN%s' "$Y" "$WARN_N" "$N"
|
||||
[ "$INFO_N" -gt 0 ] && printf ', %d INFO' "$INFO_N"
|
||||
[ "${#BLOCKERS[@]}" -gt 0 ] && printf ', %s%d BLOCKER%s' "$R" "${#BLOCKERS[@]}" "$N"
|
||||
printf '\n'
|
||||
[ "$FAIL_N" -eq 0 ]
|
||||
}
|
||||
|
||||
http_code() {
|
||||
local url="$1"; shift
|
||||
curl -skS --max-redirs 0 -m "${TIMEOUT}" -o /dev/null -w '%{http_code}' "$@" "$url" 2>/dev/null || echo 000
|
||||
}
|
||||
|
||||
http_body() {
|
||||
local url="$1" out="$2"; shift 2
|
||||
curl -skS --max-redirs 0 -m "${TIMEOUT}" -o "$out" -w '%{http_code}' "$@" "$url" 2>/dev/null || echo 000
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Currency unit map checks (wallet codec: alt_unit_names must include "0")
|
||||
# ---------------------------------------------------------------------------
|
||||
# Returns 0 if JSON body at $1 has usable alt_unit_names.
|
||||
# Supports:
|
||||
# - exchange/bank: currency_specification.alt_unit_names
|
||||
# - merchant: currencies.<CODE>.alt_unit_names for each code
|
||||
# Optional $2 = required currency code for currency_specification.currency
|
||||
json_has_alt_unit_names() {
|
||||
local file="$1" want_cur="${2:-}"
|
||||
python3 - "$file" "$want_cur" <<'PY'
|
||||
import json, sys
|
||||
path, want = sys.argv[1], sys.argv[2]
|
||||
try:
|
||||
d = json.load(open(path))
|
||||
except Exception as e:
|
||||
print(f"json-error: {e}")
|
||||
sys.exit(2)
|
||||
|
||||
def check_au(au, label):
|
||||
if not isinstance(au, dict) or not au:
|
||||
print(f"{label}: missing/empty alt_unit_names")
|
||||
return False
|
||||
if "0" not in au or not str(au.get("0") or "").strip():
|
||||
print(f"{label}: alt_unit_names missing non-empty key \"0\" (have {sorted(au.keys())})")
|
||||
return False
|
||||
print(f"{label}: alt_unit_names ok (0={au.get('0')!r}, n={len(au)})")
|
||||
return True
|
||||
|
||||
ok = True
|
||||
cs = d.get("currency_specification")
|
||||
if isinstance(cs, dict):
|
||||
if want and cs.get("currency") and cs.get("currency") != want:
|
||||
print(f"currency_specification.currency={cs.get('currency')!r} want {want!r}")
|
||||
ok = False
|
||||
if not check_au(cs.get("alt_unit_names"), "currency_specification"):
|
||||
ok = False
|
||||
elif "currency_specification" in d:
|
||||
print("currency_specification: not an object")
|
||||
ok = False
|
||||
|
||||
curs = d.get("currencies")
|
||||
if isinstance(curs, dict) and curs:
|
||||
for code, spec in curs.items():
|
||||
if not isinstance(spec, dict):
|
||||
print(f"currencies.{code}: not an object")
|
||||
ok = False
|
||||
continue
|
||||
if not check_au(spec.get("alt_unit_names"), f"currencies.{code}"):
|
||||
ok = False
|
||||
|
||||
if not isinstance(cs, dict) and not (isinstance(curs, dict) and curs):
|
||||
# neither shape — fail
|
||||
print("no currency_specification or currencies map")
|
||||
ok = False
|
||||
|
||||
sys.exit(0 if ok else 1)
|
||||
PY
|
||||
}
|
||||
|
||||
# Check one exchange base URL's /config for alt_unit_names.
|
||||
# $1=label $2=base_url $3=expected currency (optional) $4=strict(1) or soft(0)
|
||||
check_exchange_alt_units() {
|
||||
local label="$1" base="$2" want_cur="${3:-}" strict="${4:-1}"
|
||||
local f code
|
||||
base="${base%/}"
|
||||
f=$(mktemp)
|
||||
code=$(http_body "${base}/config" "$f")
|
||||
if [ "$code" != "200" ]; then
|
||||
rm -f "$f"
|
||||
if [ "$strict" = "1" ]; then
|
||||
fail "$label /config" "HTTP $code ($base)"
|
||||
else
|
||||
warn "$label /config" "HTTP $code ($base)"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
local out ec
|
||||
set +e
|
||||
out=$(json_has_alt_unit_names "$f" "$want_cur" 2>&1)
|
||||
ec=$?
|
||||
set -e
|
||||
rm -f "$f"
|
||||
if [ "$ec" -eq 0 ]; then
|
||||
ok "$label alt_unit_names" "$(echo "$out" | tr '\n' '; ' | sed 's/; $//')"
|
||||
else
|
||||
if [ "$strict" = "1" ]; then
|
||||
fail "$label alt_unit_names" "$(echo "$out" | tr '\n' '; ' | sed 's/; $//')"
|
||||
else
|
||||
warn "$label alt_unit_names" "$(echo "$out" | tr '\n' '; ' | sed 's/; $//')"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# From merchant /config JSON file: walk exchanges[] and check each base_url/config.
|
||||
# Local stack hosts (hacktivism.ch or $EXCHANGE_PUBLIC host) are strict; others soft.
|
||||
check_merchant_listed_exchanges_alt_units() {
|
||||
local mer_json="$1"
|
||||
local list
|
||||
list=$(python3 - "$mer_json" <<'PY'
|
||||
import json, sys
|
||||
d = json.load(open(sys.argv[1]))
|
||||
for e in d.get("exchanges") or []:
|
||||
if not isinstance(e, dict):
|
||||
continue
|
||||
u = (e.get("base_url") or e.get("url") or "").rstrip("/")
|
||||
c = e.get("currency") or ""
|
||||
if u:
|
||||
print(f"{c}\t{u}")
|
||||
PY
|
||||
)
|
||||
if [ -z "$list" ]; then
|
||||
fail "merchant exchanges[]" "empty — no exchanges to check for alt_unit_names"
|
||||
return
|
||||
fi
|
||||
local line cur url strict host
|
||||
while IFS=$'\t' read -r cur url; do
|
||||
[ -n "$url" ] || continue
|
||||
host="${url#https://}"; host="${host#http://}"; host="${host%%/*}"
|
||||
strict=1
|
||||
case "$host" in
|
||||
*hacktivism.ch) strict=1 ;;
|
||||
*)
|
||||
# foreign exchange (e.g. taler-ops) — soft unless it is our configured EXCHANGE_PUBLIC
|
||||
if [ "$url" = "${EXCHANGE_PUBLIC}" ] || [ "$url" = "${EXCHANGE_PUBLIC}/" ]; then
|
||||
strict=1
|
||||
else
|
||||
strict=0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
check_exchange_alt_units "exchange ${cur:-?} ${host}" "$url" "$cur" "$strict"
|
||||
done <<<"$list"
|
||||
}
|
||||
|
||||
SECRETS_ROOT="${SECRETS_ROOT:-}"
|
||||
if [ -z "$SECRETS_ROOT" ]; then
|
||||
for d in \
|
||||
"$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)/koopa-admin-secrets/koopa/host-root" \
|
||||
"/Users/newkamek/src/koopa/koopa-admin-secrets/koopa/host-root" \
|
||||
"$HOME/src/koopa/koopa-admin-secrets/koopa/host-root"
|
||||
do
|
||||
if [ -d "$d/taler-bank" ]; then SECRETS_ROOT=$d; break; fi
|
||||
done
|
||||
fi
|
||||
|
||||
read_secret() {
|
||||
local rel="$1"
|
||||
if [ -n "${SECRETS_ROOT:-}" ] && [ -f "${SECRETS_ROOT}/${rel}" ]; then
|
||||
tr -d '\n' <"${SECRETS_ROOT}/${rel}"
|
||||
return 0
|
||||
fi
|
||||
koopa_ssh_ok || return 1
|
||||
koopa_ssh_run 10 "tr -d '\\n' </root/$(basename "$rel") 2>/dev/null || true" 2>/dev/null
|
||||
}
|
||||
|
||||
find_wallet_cli() {
|
||||
if [ -n "${WALLET_CLI:-}" ] && [ -f "$WALLET_CLI" ]; then
|
||||
echo "$WALLET_CLI"; return 0
|
||||
fi
|
||||
for c in \
|
||||
/Users/newkamek/src/taler/taler-typescript-core/packages/taler-wallet-cli/bin/taler-wallet-cli.mjs \
|
||||
"$(command -v taler-wallet-cli 2>/dev/null || true)"
|
||||
do
|
||||
[ -n "$c" ] && [ -f "$c" ] && { echo "$c"; return 0; }
|
||||
done
|
||||
return 1
|
||||
}
|
||||
198
scripts/taler-monitoring/taler-monitoring.sh
Executable file
198
scripts/taler-monitoring/taler-monitoring.sh
Executable file
|
|
@ -0,0 +1,198 @@
|
|||
#!/usr/bin/env bash
|
||||
# taler-monitoring — public URL / stack checks for a Taler domain
|
||||
#
|
||||
# ./taler-monitoring.sh # local GOA (urls + inside + e2e)
|
||||
# ./taler-monitoring.sh -d taler.net urls # public demo, no SSH
|
||||
# ./taler-monitoring.sh --domain taler-ops.ch # public ops, no SSH
|
||||
# TALER_DOMAIN=demo.taler.net ./taler-monitoring.sh urls
|
||||
#
|
||||
# Tags: [OK] [INFO] [WARN] [ERROR] [BLOCKER]
|
||||
# Exit 0 only if every selected phase exits 0.
|
||||
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
taler-monitoring — bank / exchange / merchant checks
|
||||
|
||||
Usage:
|
||||
./taler-monitoring.sh [options] [phases...]
|
||||
|
||||
Phases:
|
||||
urls public HTTPS (no SSH) ← default with --domain
|
||||
inside container status via SSH (local stack only)
|
||||
versions taler packages vs deb.taler.net trixie + repo availability
|
||||
sanity public + optional server
|
||||
server server-side only (SSH)
|
||||
e2e withdraw + pay (small amounts; remote aborts on login/KYC)
|
||||
all urls + inside + versions + sanity + e2e (SSH phases only on koopa)
|
||||
|
||||
Options:
|
||||
-d, --domain DOMAIN target domain
|
||||
koopa / hacktivism.ch → local stack, SSH ok
|
||||
everything else → no SSH; e2e optional (tiny amounts)
|
||||
presets: koopa | hacktivism.ch | taler.net | taler-ops.ch
|
||||
generic: bank/exchange/backend.<domain>
|
||||
--bank URL override bank base (https://…)
|
||||
--exchange URL override exchange base
|
||||
--merchant URL override merchant base
|
||||
--currency CODE expected currency (GOA, KUDOS, CHF, …); empty = report only
|
||||
--no-probe do not probe alternate merchant/bank hostnames
|
||||
-h, --help
|
||||
|
||||
Examples:
|
||||
./taler-monitoring.sh -d taler.net
|
||||
./taler-monitoring.sh -d taler-ops.ch urls
|
||||
./taler-monitoring.sh -d demo.taler.net --currency KUDOS
|
||||
./taler-monitoring.sh --exchange https://exchange.taler-ops.ch urls
|
||||
|
||||
Env (same meaning):
|
||||
TALER_DOMAIN BANK_PUBLIC EXCHANGE_PUBLIC MERCHANT_PUBLIC EXPECT_CURRENCY
|
||||
SKIP_SSH=1 NO_COLOR=1
|
||||
EOF
|
||||
}
|
||||
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
|
||||
PHASES=()
|
||||
DOMAIN_SET=0
|
||||
BANK_OVERRIDE=""
|
||||
EXCHANGE_OVERRIDE=""
|
||||
MERCHANT_OVERRIDE=""
|
||||
CURRENCY_OVERRIDE=""
|
||||
NO_PROBE=0
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-d|--domain)
|
||||
[ $# -ge 2 ] || { echo "missing arg for $1" >&2; exit 2; }
|
||||
apply_taler_domain "$2"
|
||||
DOMAIN_SET=1
|
||||
shift 2
|
||||
;;
|
||||
--bank)
|
||||
[ $# -ge 2 ] || { echo "missing arg for $1" >&2; exit 2; }
|
||||
BANK_OVERRIDE="${2%/}"; shift 2
|
||||
;;
|
||||
--exchange)
|
||||
[ $# -ge 2 ] || { echo "missing arg for $1" >&2; exit 2; }
|
||||
EXCHANGE_OVERRIDE="${2%/}"; shift 2
|
||||
;;
|
||||
--merchant)
|
||||
[ $# -ge 2 ] || { echo "missing arg for $1" >&2; exit 2; }
|
||||
MERCHANT_OVERRIDE="${2%/}"; shift 2
|
||||
;;
|
||||
--currency)
|
||||
[ $# -ge 2 ] || { echo "missing arg for $1" >&2; exit 2; }
|
||||
CURRENCY_OVERRIDE="$2"; shift 2
|
||||
;;
|
||||
--no-probe) NO_PROBE=1; shift ;;
|
||||
urls|inside|versions|sanity|server|e2e|all) PHASES+=("$1"); shift ;;
|
||||
*)
|
||||
# bare domain shorthand: ./taler-monitoring.sh taler.net
|
||||
if [[ "$1" == *.* && "$1" != *://* && "$1" != -* ]]; then
|
||||
apply_taler_domain "$1"
|
||||
DOMAIN_SET=1
|
||||
shift
|
||||
else
|
||||
echo "Unknown: $1" >&2; usage >&2; exit 2
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$NO_PROBE" = "1" ]; then
|
||||
TALER_DOMAIN_PROBE=0
|
||||
fi
|
||||
if [ -n "$CURRENCY_OVERRIDE" ]; then
|
||||
EXPECT_CURRENCY="$CURRENCY_OVERRIDE"
|
||||
fi
|
||||
if [ -n "$BANK_OVERRIDE" ]; then BANK_PUBLIC="$BANK_OVERRIDE"; fi
|
||||
if [ -n "$EXCHANGE_OVERRIDE" ]; then EXCHANGE_PUBLIC="$EXCHANGE_OVERRIDE"; fi
|
||||
if [ -n "$MERCHANT_OVERRIDE" ]; then MERCHANT_PUBLIC="$MERCHANT_OVERRIDE"; fi
|
||||
|
||||
# Only koopa may use SSH. Remote domains: public + optional e2e (no SSH).
|
||||
if [ "${LOCAL_STACK:-1}" != "1" ]; then
|
||||
SKIP_SSH=1
|
||||
fi
|
||||
|
||||
# Remote: no SSH; ATM withdraw ladder set in check_e2e (smaller notes)
|
||||
if [ "${LOCAL_STACK}" != "1" ]; then
|
||||
E2E_FAKE_INCOMING=0
|
||||
E2E_REMOTE=1
|
||||
: "${E2E_WITHDRAW_VALUES:=10 20 50}"
|
||||
: "${E2E_PAY_VALUES:=0.01 0.05 0.1 1}"
|
||||
fi
|
||||
|
||||
# Export so check_*.sh (re-source lib) see the same targets via env
|
||||
export TALER_DOMAIN BANK_PUBLIC EXCHANGE_PUBLIC MERCHANT_PUBLIC
|
||||
export EXPECT_CURRENCY SKIP_SSH LOCAL_STACK TALER_DOMAIN_PROBE
|
||||
export WITHDRAW_AMT PAY_AMT CREDIT_AMT MERCHANT_INSTANCE
|
||||
export E2E_FAKE_INCOMING E2E_REMOTE E2E_VARIABLE E2E_ATM_MAX
|
||||
export E2E_WITHDRAW_VALUES E2E_PAY_VALUES
|
||||
export TALER_DOMAIN_APPLIED=1
|
||||
|
||||
# Default phases
|
||||
if [ "${#PHASES[@]}" -eq 0 ]; then
|
||||
if [ "${LOCAL_STACK}" = "1" ]; then
|
||||
PHASES=(urls inside versions e2e) # koopa: SSH ok
|
||||
else
|
||||
PHASES=(urls) # remote default: public only; pass "e2e" to try
|
||||
fi
|
||||
fi
|
||||
|
||||
OUT_PHASES=()
|
||||
for p in "${PHASES[@]}"; do
|
||||
if [ "$p" = "all" ]; then
|
||||
if [ "${LOCAL_STACK}" = "1" ]; then
|
||||
OUT_PHASES+=(urls inside versions sanity e2e)
|
||||
else
|
||||
# remote: repo index check only (no installed pkgs without SSH)
|
||||
OUT_PHASES+=(urls versions e2e)
|
||||
fi
|
||||
else
|
||||
OUT_PHASES+=("$p")
|
||||
fi
|
||||
done
|
||||
PHASES=()
|
||||
seen=" "
|
||||
for p in "${OUT_PHASES[@]}"; do
|
||||
# Drop SSH-only phases for non-koopa (e2e is allowed — public path only)
|
||||
# versions still runs: outside-in deb.taler.net checks; container parts soft-skip if no SSH
|
||||
if [ "${LOCAL_STACK}" != "1" ] || [ "${SKIP_SSH}" = "1" ]; then
|
||||
case "$p" in inside|server)
|
||||
echo "[INFO] skip phase '$p' (only koopa uses SSH)" >&2
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
case "$seen" in *" $p "*) ;; *) PHASES+=("$p"); seen="$seen$p " ;; esac
|
||||
done
|
||||
|
||||
if [ "${#PHASES[@]}" -eq 0 ]; then
|
||||
PHASES=(urls)
|
||||
fi
|
||||
|
||||
printf 'target domain=%s\n' "${TALER_DOMAIN}"
|
||||
printf ' bank %s\n' "$BANK_PUBLIC"
|
||||
printf ' exchange %s\n' "$EXCHANGE_PUBLIC"
|
||||
printf ' merchant %s\n' "$MERCHANT_PUBLIC"
|
||||
printf ' currency expect=%s skip_ssh=%s\n' "${EXPECT_CURRENCY:-any}" "$SKIP_SSH"
|
||||
|
||||
chmod +x "$ROOT"/check_*.sh 2>/dev/null || true
|
||||
|
||||
ec=0
|
||||
for p in "${PHASES[@]}"; do
|
||||
case "$p" in
|
||||
urls) "$ROOT/check_urls.sh" || ec=1 ;;
|
||||
inside) "$ROOT/check_inside.sh" || ec=1 ;;
|
||||
versions) "$ROOT/check_versions.sh" || ec=1 ;;
|
||||
sanity) "$ROOT/check_sanity.sh" || ec=1 ;;
|
||||
server) "$ROOT/check_server.sh" || ec=1 ;;
|
||||
e2e) "$ROOT/check_e2e.sh" || ec=1 ;;
|
||||
esac
|
||||
done
|
||||
exit "$ec"
|
||||
Loading…
Add table
Add a link
Reference in a new issue