Add bank maintenance script to raise all account debit limits.

mytops-style dry-run tool; PATCH debit_threshold for every libeufin account.
This commit is contained in:
Hernâni Marques 2026-07-16 21:17:43 +02:00
parent 97b2fcce46
commit 36ef637501
3 changed files with 337 additions and 0 deletions

View file

@ -5,6 +5,8 @@
| `taler-merchant/` | podman `taler-hacktivism`: `/root`, `/usr/local/bin` | | `taler-merchant/` | podman `taler-hacktivism`: `/root`, `/usr/local/bin` |
| `taler-exchange/` | podman `taler-hacktivism-exchange-ansible`: `/root`, `/usr/local/bin` | | `taler-exchange/` | podman `taler-hacktivism-exchange-ansible`: `/root`, `/usr/local/bin` |
| `taler-hacktivism-bank/` | podman `taler-hacktivism-bank`: `/root`, `/usr/local/bin` | | `taler-hacktivism-bank/` | podman `taler-hacktivism-bank`: `/root`, `/usr/local/bin` |
| `taler-bank/` | bank host helpers + **`maintenance/`** (debit limits, dry-run) |
| `taler-shared/` | host **ensure-taler-apps** (post-container app start + auto-confirm) |
| `taler-sanity/` | host root checks (stack, settlement, helpers) | | `taler-sanity/` | host root checks (stack, settlement, helpers) |
| `taler-monitoring/` | **outside-in** public URL walk (`/config` → keys/terms/integration/webui) | | `taler-monitoring/` | **outside-in** public URL walk (`/config` → keys/terms/integration/webui) |
| `monitoring/` | host `/home/hernani/scripts` (tor relay stats) | | `monitoring/` | host `/home/hernani/scripts` (tor relay stats) |
@ -14,6 +16,14 @@
**Secrets:** never in this tree — sibling **`../koopa-admin-secrets`** (`koopa/host-root/<service>/``/root/` on host; `containers/…/secrets/` for in-container). **Secrets:** never in this tree — sibling **`../koopa-admin-secrets`** (`koopa/host-root/<service>/``/root/` on host; `containers/…/secrets/` for in-container).
## Boot automation (host user systemd)
Merchant/bank containers stay **`sleep infinity`**. After the container unit
starts, **`taler-merchant-apps.service`** / **`taler-bank-apps.service`** run
`~/.local/bin/ensure-taler-apps.sh` (install via
`taler-shared/install-ensure-taler-apps.sh`). That runs the in-container
`start_base` + `start_*.sh` (and bank auto-confirm **2 s**).
## Manual start model (all three) ## Manual start model (all three)
1. **root** runs `/root/start_base_services_for_taler_*.sh` 1. **root** runs `/root/start_base_services_for_taler_*.sh`

View file

@ -0,0 +1,47 @@
# taler-bank maintenance
One-shot ops tools for **libeufin-bank** on koopa (`taler-hacktivism-bank`).
Pattern matches **mytops-admin-log** `scripts/taler-merchant/maintenance/`: **dry-run by default**, `--no-dry` to apply.
## `raise-debit-limits.sh`
Raise **`debit_threshold`** on every bank account (overdraft / withdraw headroom).
Optionally set **`DEFAULT_DEBT_LIMIT`** for new accounts.
| | |
|--|--|
| **Default amount** | `GOA:4503599627370496` (libeufin amount ceiling ≈ 2⁵²) |
| **Auth** | admin password: `BANK_ADMIN_PASS` or `bank-admin-password.txt` |
| **Bank** | `http://127.0.0.1:9012` on koopa, or `podman exec` into `taler-hacktivism-bank` |
### Usage
```bash
# on koopa (hernani), after bank is up:
cd ~/path/to/koopa-admin-log # or copy script
# preview
./scripts/taler-bank/maintenance/raise-debit-limits.sh
# apply to all accounts
./scripts/taler-bank/maintenance/raise-debit-limits.sh --no-dry
# apply + DEFAULT_DEBT_LIMIT in container overrides + bank restart
./scripts/taler-bank/maintenance/raise-debit-limits.sh --no-dry --set-default
# custom amount / single user
./scripts/taler-bank/maintenance/raise-debit-limits.sh --no-dry --amount GOA:1000000 --only explorer
```
Via container if host cannot reach :9012:
```bash
podman exec -u root -i taler-hacktivism-bank bash -s -- --no-dry \
< scripts/taler-bank/maintenance/raise-debit-limits.sh
```
### Notes
- Does **not** invent balance; only raises how far accounts may go into **debit**.
- Exchange coin denominations (largest `GOA:1000`) are separate — large withdraws still work with many coins.
- Secrets never live in this repo — see `SECRETS.md` / `koopa-admin-secrets`.

View file

@ -0,0 +1,280 @@
#!/usr/bin/env bash
# Raise libeufin-bank debit thresholds for all accounts (and optionally the default).
#
# Style: like mytops-admin-log scripts/taler-merchant/maintenance/*.sh
# dry-run by default; pass --no-dry to apply.
#
# Where to run:
# - on koopa host (preferred): talks to BANK_URL (default http://127.0.0.1:9012)
# - or: podman exec -u root -i taler-hacktivism-bank bash -s < this-script -- --no-dry
#
# Auth (first match wins):
# BANK_ADMIN_PASS
# /root/bank-admin-password.txt (inside bank container or host root secrets mirror)
# $KOOPA_SECRETS/.../bank-admin-password.txt
# ../koopa-admin-secrets/koopa/host-root/taler-bank/bank-admin-password.txt (relative to repo)
#
# Env / flags:
# --no-dry apply changes
# --amount GOA:N threshold (default: libeufin ceiling GOA:4503599627370496)
# --set-default also set DEFAULT_DEBT_LIMIT in bank-overrides.conf (+ bank restart)
# --only USER[,USER] only these usernames
# BANK_URL default http://127.0.0.1:9012
# BANK_CTR if set and BANK not reachable on host, use podman exec into container
# (default: taler-hacktivism-bank when host curl fails)
set -euo pipefail
echo "[INFO] raise-debit-limits — libeufin-bank account debit_threshold"
echo "[INFO] Dry-run unless --no-dry. Tested against bank.hacktivism.ch (GOA)."
DRY_RUN=true
SET_DEFAULT=false
AMOUNT="${AMOUNT:-GOA:4503599627370496}"
ONLY_USERS=""
BANK_URL="${BANK_URL:-http://127.0.0.1:9012}"
BANK_URL="${BANK_URL%/}"
BANK_CTR="${BANK_CTR:-taler-hacktivism-bank}"
OVERRIDE_CONF="${OVERRIDE_CONF:-/etc/libeufin/bank-overrides.conf}"
usage() {
sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'
exit 0
}
while [[ $# -gt 0 ]]; do
case "$1" in
--no-dry) DRY_RUN=false; echo "[WARN] Dry-run disabled — will PATCH accounts."; shift ;;
--set-default) SET_DEFAULT=true; shift ;;
--amount) AMOUNT="${2:?}"; shift 2 ;;
--only) ONLY_USERS="${2:?}"; shift 2 ;;
-h|--help) usage ;;
*)
echo "[ERROR] unknown arg: $1" >&2
exit 2
;;
esac
done
if $DRY_RUN; then
echo "[INFO] Dry-run active (no HTTP PATCH, no conf write)."
else
echo "[WARN] APPLY mode amount=${AMOUNT}"
fi
# --- admin password ---
find_admin_pass() {
if [[ -n "${BANK_ADMIN_PASS:-}" ]]; then
printf '%s' "$BANK_ADMIN_PASS"
return 0
fi
local f
for f in \
/root/bank-admin-password.txt \
"${KOOPA_SECRETS:-}/koopa/host-root/taler-bank/bank-admin-password.txt" \
"$(cd "$(dirname "$0")/../../../.." 2>/dev/null && pwd)/koopa-admin-secrets/koopa/host-root/taler-bank/bank-admin-password.txt" \
"$HOME/src/koopa/koopa-admin-secrets/koopa/host-root/taler-bank/bank-admin-password.txt"
do
[[ -n "$f" && -f "$f" && -r "$f" ]] || continue
tr -d '\n' <"$f"
return 0
done
# hernani@koopa: secret lives in the bank container
if command -v podman >/dev/null 2>&1 \
&& podman inspect -f '{{.State.Running}}' "${BANK_CTR:-taler-hacktivism-bank}" 2>/dev/null | grep -qx true; then
podman exec "${BANK_CTR:-taler-hacktivism-bank}" cat /root/bank-admin-password.txt 2>/dev/null | tr -d '\n'
return 0
fi
return 1
}
ADMIN_PASS="$(find_admin_pass)" || {
echo "[ERROR] no admin password (BANK_ADMIN_PASS or bank-admin-password.txt)" >&2
exit 1
}
# Prefer host URL; fall back to podman exec curl inside bank container
USE_PODMAN=false
if ! curl -sf -m 3 "${BANK_URL}/config" >/dev/null 2>&1; then
if command -v podman >/dev/null 2>&1 && podman inspect -f '{{.State.Running}}' "$BANK_CTR" 2>/dev/null | grep -qx true; then
echo "[INFO] ${BANK_URL} not reachable — using podman exec ${BANK_CTR}"
USE_PODMAN=true
BANK_URL="http://127.0.0.1:9012"
else
echo "[ERROR] bank not reachable at ${BANK_URL} and container ${BANK_CTR} not running" >&2
exit 1
fi
fi
bank_curl() {
# bank_curl [curl-args...] URL_PATH_or_absolute
# Last arg is URL path starting with / or full URL
local args=("$@")
local n=$((${#args[@]} - 1))
local url="${args[$n]}"
unset "args[$n]"
if [[ "$url" != http* ]]; then
url="${BANK_URL}${url}"
fi
if $USE_PODMAN; then
podman exec -u root -i "$BANK_CTR" curl -sS -m 30 "${args[@]}" "$url"
else
curl -sS -m 30 "${args[@]}" "$url"
fi
}
bank_curl_code() {
# like bank_curl but print HTTP code on stdout after body to fd3... simpler: write body to file
local out="$1"; shift
local args=("$@")
local n=$((${#args[@]} - 1))
local url="${args[$n]}"
unset "args[$n]"
if [[ "$url" != http* ]]; then
url="${BANK_URL}${url}"
fi
if $USE_PODMAN; then
podman exec -u root -i "$BANK_CTR" curl -sS -m 30 -o /tmp/raise-debt-body -w '%{http_code}' "${args[@]}" "$url"
else
curl -sS -m 30 -o "$out" -w '%{http_code}' "${args[@]}" "$url"
fi
}
echo "[INFO] bank=${BANK_URL} podman=${USE_PODMAN} amount=${AMOUNT}"
TOK_JSON=$(bank_curl -u "admin:${ADMIN_PASS}" -H 'Content-Type: application/json' \
-d '{"scope":"readwrite"}' /accounts/admin/token)
TOKEN=$(printf '%s' "$TOK_JSON" | python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')
[[ -n "$TOKEN" ]] || { echo "[ERROR] admin token failed: $TOK_JSON" >&2; exit 1; }
echo "[INFO] admin token OK"
ACCS_JSON=$(bank_curl -H "Authorization: Bearer ${TOKEN}" "/accounts?limit=500")
export ACCS_JSON AMOUNT ONLY_USERS DRY_RUN TOKEN
export BANK_URL USE_PODMAN BANK_CTR
python3 <<'PY'
import json, os, sys, subprocess, urllib.request
amount = os.environ["AMOUNT"]
only = {u.strip() for u in os.environ.get("ONLY_USERS", "").split(",") if u.strip()}
dry = os.environ.get("DRY_RUN", "true").lower() in ("1", "true", "yes")
token = os.environ["TOKEN"]
bank = os.environ["BANK_URL"].rstrip("/")
use_podman = os.environ.get("USE_PODMAN", "false").lower() in ("1", "true", "yes")
ctr = os.environ.get("BANK_CTR", "taler-hacktivism-bank")
accs = json.loads(os.environ["ACCS_JSON"]).get("accounts") or []
if only:
accs = [a for a in accs if a.get("username") in only]
print(f"[INFO] accounts to process: {len(accs)}")
def http_patch(user: str, body: dict) -> int:
data = json.dumps(body).encode()
url = f"{bank}/accounts/{user}"
if use_podman:
# avoid putting token in process list longer than needed — still visible
cmd = [
"podman", "exec", "-u", "root", "-i", ctr,
"curl", "-sS", "-m", "30", "-o", "/dev/null", "-w", "%{http_code}",
"-X", "PATCH",
"-H", f"Authorization: Bearer {token}",
"-H", "Content-Type: application/json",
"-d", json.dumps(body),
url.replace(bank, "http://127.0.0.1:9012") if bank.startswith("http") else f"http://127.0.0.1:9012/accounts/{user}",
]
# always use in-container localhost for podman path
cmd[-1] = f"http://127.0.0.1:9012/accounts/{user}"
out = subprocess.check_output(cmd, text=True).strip()
return int(out)
req = urllib.request.Request(
url,
data=data,
method="PATCH",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
)
try:
with urllib.request.urlopen(req, timeout=30) as r:
return r.status
except Exception as e:
code = getattr(e, "code", None)
if code is not None:
return int(code)
print(f"[ERROR] {user}: {e}", file=sys.stderr)
return 0
ok = fail = skip = 0
for a in accs:
u = a["username"]
old = a.get("debit_threshold") or "?"
bal = a.get("balance") or {}
if old == amount:
print(f"[SKIP] {u}: already {amount}")
skip += 1
continue
if dry:
print(f"[DRY] {u}: {old} -> {amount} (balance={bal})")
ok += 1
continue
code = http_patch(u, {"debit_threshold": amount})
if code in (200, 204):
print(f"[EXECUTE] {u}: {old} -> {amount} HTTP {code}")
ok += 1
else:
print(f"[FAIL] {u}: {old} -> {amount} HTTP {code}")
fail += 1
print(f"[INFO] done ok={ok} skip={skip} fail={fail} dry={dry}")
sys.exit(1 if fail else 0)
PY
# Optional: DEFAULT_DEBT_LIMIT in overrides (new accounts)
if $SET_DEFAULT; then
echo "[INFO] --set-default: DEFAULT_DEBT_LIMIT in ${OVERRIDE_CONF}"
if $USE_PODMAN; then
conf_path="$OVERRIDE_CONF"
if $DRY_RUN; then
echo "[DRY] sed DEFAULT_DEBT_LIMIT = ${AMOUNT} in container:${conf_path}"
echo "[DRY] restart libeufin-bank serve"
else
podman exec -u root "$BANK_CTR" bash -lc "
set -e
conf='$OVERRIDE_CONF'
cp -a \"\$conf\" \"\${conf}.bak-\$(date +%Y%m%d%H%M%S)\"
if grep -q '^DEFAULT_DEBT_LIMIT' \"\$conf\"; then
sed -i 's/^DEFAULT_DEBT_LIMIT = .*/DEFAULT_DEBT_LIMIT = ${AMOUNT}/' \"\$conf\"
else
echo 'DEFAULT_DEBT_LIMIT = ${AMOUNT}' >>\"\$conf\"
fi
grep '^DEFAULT_DEBT_LIMIT' \"\$conf\"
# restart bank process (manual stack)
pids=\$(ps -eo pid=,args= | awk '/libeufin-bank serve/ && !/awk/ {print \$1}')
for p in \$pids; do kill \$p 2>/dev/null || true; done
sleep 1
runuser -u libeufin-bank -- nohup /usr/bin/libeufin-bank serve -c /etc/libeufin/libeufin-bank.conf \
>>/var/log/libeufin-bank/serve.log 2>&1 &
for i in \$(seq 1 30); do
curl -sf -m 2 http://127.0.0.1:9012/config >/dev/null && break
sleep 0.5
done
curl -sS http://127.0.0.1:9012/config | python3 -c 'import sys,json; d=json.load(sys.stdin); print(\"[INFO] default_debit_threshold\", d.get(\"default_debit_threshold\"))'
"
echo "[EXECUTE] DEFAULT_DEBT_LIMIT + bank restart"
fi
else
if [[ ! -f "$OVERRIDE_CONF" ]]; then
echo "[WARN] ${OVERRIDE_CONF} not on host — skip --set-default (use from inside container or with podman path)"
elif $DRY_RUN; then
echo "[DRY] sed DEFAULT_DEBT_LIMIT = ${AMOUNT} in ${OVERRIDE_CONF}"
else
cp -a "$OVERRIDE_CONF" "${OVERRIDE_CONF}.bak-$(date +%Y%m%d%H%M%S)"
sed -i "s/^DEFAULT_DEBT_LIMIT = .*/DEFAULT_DEBT_LIMIT = ${AMOUNT}/" "$OVERRIDE_CONF"
echo "[EXECUTE] wrote ${OVERRIDE_CONF}"
grep '^DEFAULT_DEBT_LIMIT' "$OVERRIDE_CONF" || true
echo "[WARN] restart libeufin-bank yourself if conf is mounted from host"
fi
fi
fi
echo "[INFO] Done."