koopa-admin-log/scripts/taler-monitoring/check_versions.sh

469 lines
16 KiB
Bash
Executable file

#!/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