#!/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 $(…)</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