#!/usr/bin/env bash set -Eeuo pipefail # ---------------------------- # Logging (ALLES nach STDERR!) # ---------------------------- _ts() { date '+%F %T'; } log() { echo "[$(_ts)] $*" >&2; } info() { log "INFO: $*"; } warn() { log "WARN: $*"; } die() { log "ERROR: $*"; exit 1; } setup_traps() { trap 'die "Failed at line ${LINENO}: ${BASH_COMMAND} (exit=$?)"' ERR } need_cmd() { local c for c in "$@"; do command -v "$c" >/dev/null 2>&1 || die "Missing command: $c" done } # ---------------------------- # Proxmox helpers # ---------------------------- pve_storage_exists() { local st="$1" pvesm status --storage "$st" >/dev/null 2>&1 } pve_bridge_exists() { local br="$1" [[ -d "/sys/class/net/${br}/bridge" ]] } # Return list of all VMIDs in the cluster (CT + VM), one per line. # No pipelines to "tail" etc. in-script -> avoids broken pipe. pve_cluster_vmids() { need_cmd pvesh python3 local json json="$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null || true)" [[ -n "$json" ]] || return 0 python3 - <<'PY' <<<"$json" import json, sys try: data = json.loads(sys.stdin.read()) except Exception: sys.exit(0) for r in data: vmid = r.get("vmid") if isinstance(vmid, int): print(vmid) PY } pve_vmid_exists_cluster() { local vmid="$1" [[ "$vmid" =~ ^[0-9]+$ ]] || return 1 pve_cluster_vmids | awk -v id="$vmid" '$1==id{found=1} END{exit found?0:1}' } # Customer-safe CTID: # epoch - 1000000000 => e.g. 1768138201 -> 768138201 pve_select_customer_ctid() { need_cmd date local base base="$(date +%s)" local ctid=$((base - 1000000000)) # ensure integer + not used while pve_vmid_exists_cluster "$ctid"; do ctid=$((ctid + 1)) done echo "$ctid" } # Ensure Debian 12 template exists and return "storage:vztmpl/" on STDOUT ONLY. pve_template_ensure_debian12() { need_cmd pveam awk grep local preferred_store="$1" local tpl="debian-12-standard_12.12-1_amd64.tar.zst" local store="$preferred_store" # Some storages (e.g. local-zfs) don't support templates in pveam. if ! pveam list "$store" >/dev/null 2>&1; then warn "pveam storage '$store' not available for templates; falling back to 'local'" store="local" fi # Make sure template exists; 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 update >/dev/null 2>&1 || true pveam download "$store" "$tpl" >/dev/null fi # IMPORTANT: only echo the template ref on STDOUT echo "${store}:vztmpl/${tpl}" } # Build net0 string, supports VLAN tag pve_build_net0() { local bridge="$1" local ipcfg="$2" local vlan="${3:-}" local base="name=eth0,bridge=${bridge},ip=${ipcfg}" if [[ -n "$vlan" ]]; then echo "${base},tag=${vlan}" else echo "${base}" fi } # Wait for IPv4 on eth0 pct_wait_for_ip() { need_cmd pct awk cut head sleep local ctid="$1" local tries="${2:-60}" local i ip for i in $(seq 1 "$tries"); do ip="$(pct exec "$ctid" -- bash -lc "ip -4 -o addr show dev eth0 2>/dev/null | awk '{print \$4}' | cut -d/ -f1 | head -n1" 2>/dev/null || true)" if [[ -n "$ip" ]]; then echo "$ip" return 0 fi sleep 1 done return 1 } pct_exec() { local ctid="$1"; shift pct exec "$ctid" -- bash -lc "$*" } # Generate secrets without pipelines that trigger broken-pipe noise rand_hex_32() { need_cmd openssl openssl rand -hex 32 } rand_pw_32() { need_cmd openssl # URL-safe-ish openssl rand -base64 32 | tr -d '\n' | tr '/+' 'Aa' | cut -c1-32 }