diff --git a/scripts/README.md b/scripts/README.md index 5570301..70922e5 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -5,6 +5,8 @@ | `taler-merchant/` | podman `taler-hacktivism`: `/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-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-monitoring/` | **outside-in** public URL walk (`/config` → keys/terms/integration/webui) | | `monitoring/` | host `/home/hernani/scripts` (tor relay stats) | @@ -14,6 +16,14 @@ **Secrets:** never in this tree — sibling **`../koopa-admin-secrets`** (`koopa/host-root//` ↔ `/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) 1. **root** runs `/root/start_base_services_for_taler_*.sh` diff --git a/scripts/taler-bank/maintenance/README.md b/scripts/taler-bank/maintenance/README.md new file mode 100644 index 0000000..b129260 --- /dev/null +++ b/scripts/taler-bank/maintenance/README.md @@ -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`. diff --git a/scripts/taler-bank/maintenance/raise-debit-limits.sh b/scripts/taler-bank/maintenance/raise-debit-limits.sh new file mode 100755 index 0000000..648924c --- /dev/null +++ b/scripts/taler-bank/maintenance/raise-debit-limits.sh @@ -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."