#!/usr/bin/env bash # libsupabase.sh set -Eeuo pipefail # ---------- logging (ALLES nach STDERR) ---------- _ts() { date '+%F %T'; } _log() { local level="$1"; shift # stderr, damit stdout sauber bleibt (für JSON am Ende) echo "[$(_ts)] ${level}: $*" >&2 } info() { _log "INFO" "$*"; } warn() { _log "WARN" "$*"; } err() { _log "ERROR" "$*"; } die() { err "$*" exit 1 } need_cmd() { local c for c in "$@"; do command -v "$c" >/dev/null 2>&1 || die "Missing command: $c" done } on_error() { local lineno="$1" cmd="$2" code="$3" err "Failed at line ${lineno}: ${cmd} (exit=${code})" exit 1 } setup_traps() { trap 'on_error "${LINENO}" "${BASH_COMMAND}" "$?"' ERR } # ---------- helpers ---------- is_int() { [[ "${1:-}" =~ ^[0-9]+$ ]]; } # pct exec wrapper # - setzt Locale für apt/Perl stabil auf C.UTF-8 während Provisioning pct_exec() { local ctid="$1"; shift need_cmd pct # shellcheck disable=SC2029 pct exec "${ctid}" -- bash -lc "export LANG=C.UTF-8 LC_ALL=C.UTF-8; $*" } # wait for IP via pct (DHCP) pct_wait_for_ip() { local ctid="$1" need_cmd pct awk grep local i ip for i in {1..60}; do # pct exec ip addr ist zuverlässiger als pct config parsing ip="$(pct exec "${ctid}" -- bash -lc "ip -4 -o addr show scope global | awk '{print \$4}' | head -n1 | cut -d/ -f1" 2>/dev/null || true)" if [[ -n "${ip}" ]]; then echo "${ip}" return 0 fi sleep 1 done return 1 } # Proxmox checks pve_storage_exists() { local st="$1" need_cmd pvesm pvesm status --storage "${st}" >/dev/null 2>&1 } pve_bridge_exists() { local br="$1" [[ -d "/sys/class/net/${br}/bridge" ]] } # Build net0 string (supports optional VLAN tag) pve_build_net0() { local bridge="$1" local ipcfg="$2" # dhcp OR CIDR local vlan="${3:-}" # optional integer local net="name=eth0,bridge=${bridge}" if [[ -n "${vlan}" ]]; then is_int "${vlan}" || die "--vlan must be integer" net="${net},tag=${vlan}" fi if [[ "${ipcfg}" == "dhcp" ]]; then net="${net},ip=dhcp" else net="${net},ip=${ipcfg}" fi echo "${net}" } # Template ensure (stdout darf NUR den Template-Pfad liefern!) pve_template_ensure_debian12() { local preferred_store="$1" local tpl="debian-12-standard_12.12-1_amd64.tar.zst" need_cmd pveam awk grep local store="${preferred_store}" # pveam kann oft nur "local" if ! pveam list "${store}" >/dev/null 2>&1; then warn "pveam storage '${store}' not available for templates; falling back to 'local'" store="local" fi # update list pveam update >/dev/null 2>&1 || true # download if missing if ! pveam list "${store}" 2>/dev/null | awk '{print $2}' | grep -qx "${tpl}"; then info "Downloading CT template to ${store}: ${tpl}" pveam download "${store}" "${tpl}" >/dev/null fi echo "${store}:vztmpl/${tpl}" } # ---- CTID selection (customer-safe): (unix_time - 1_000_000_000) ---- # Keine Cluster-Abfrage mehr nötig. Nur lokale Node-Check, + increment falls belegt. pve_select_customer_ctid() { need_cmd pct date local base base="$(($(date +%s) - 1000000000))" # falls negative (sollte nicht passieren), fallback if (( base < 100 )); then base=100000 fi local ctid="${base}" # nur lokal prüfen (pct list ist node-lokal) while pct status "${ctid}" >/dev/null 2>&1; do ctid="$((ctid + 1))" done echo "${ctid}" } # Secrets ohne tr/head Pipes (kein Broken pipe) gen_hex() { local nbytes="$1" is_int "${nbytes}" || die "gen_hex expects integer bytes" need_cmd openssl openssl rand -hex "${nbytes}" } # JSON escaper (minimal, reicht für unsere Werte) json_escape() { local s="${1:-}" s="${s//\\/\\\\}" s="${s//\"/\\\"}" s="${s//$'\n'/\\n}" s="${s//$'\r'/\\r}" s="${s//$'\t'/\\t}" echo -n "${s}" } # Print a JSON object (stdout). Use at the VERY end. print_json_kv() { # args: key value local k="$1" v="$2" echo -n "\"$(json_escape "$k")\":\"$(json_escape "$v")\"" }