#!/usr/bin/env bash set -Eeuo pipefail # --------------------------- # Logging (ALLES nach stderr) # --------------------------- _ts() { date +"%F %T"; } info() { echo "[$(_ts)] INFO: $*" >&2; } warn() { echo "[$(_ts)] WARN: $*" >&2; } err() { echo "[$(_ts)] ERROR: $*" >&2; } die() { err "$*"; exit 1; } setup_traps() { trap 'rc=$?; err "Failed at line ${BASH_LINENO[0]}: ${BASH_COMMAND} (exit=${rc})"; exit ${rc}' 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:?storage}" pvesm status -content rootdir,vztmpl 2>/dev/null | awk '{print $1}' | grep -qx "$st" } pve_bridge_exists() { local br="${1:?bridge}" ip link show "$br" >/dev/null 2>&1 } # Ensure Debian 12 template exists. # IMPORTANT: stdout MUST be ONLY the template path. pve_template_ensure_debian12() { local preferred_storage="${1:?storage}" local tmpl="debian-12-standard_12.12-1_amd64.tar.zst" local tmpl_path="" # pveam templates are stored on storages that have 'vztmpl' content type. # Many setups only allow 'local' for templates. if pveam available -section system 2>/dev/null | awk '{print $2}' | grep -qx "$tmpl"; then : else warn "Could not verify template list via pveam available; continuing anyway." fi # Decide template storage: prefer given storage if it supports vztmpl; else fallback to 'local' if pvesm status -content vztmpl 2>/dev/null | awk '{print $1}' | grep -qx "$preferred_storage"; then : else warn "pveam storage '${preferred_storage}' not available for templates; falling back to 'local'" preferred_storage="local" fi # Download if missing if ! pvesm list "${preferred_storage}" 2>/dev/null | awk '{print $1}' | grep -q "vztmpl/${tmpl}$"; then info "Downloading CT template to ${preferred_storage}: ${tmpl}" pveam download "${preferred_storage}" "${tmpl}" >/dev/null else info "CT template already present on ${preferred_storage}: ${tmpl}" fi tmpl_path="${preferred_storage}:vztmpl/${tmpl}" echo "${tmpl_path}" } # Build net0 string with optional VLAN tag and ip config. # IPCFG: "dhcp" or "X.X.X.X/YY" # VLAN: empty or integer pve_build_net0() { local bridge="${1:?bridge}" local ipcfg="${2:?ipcfg}" local vlan="${3:-}" local net="name=eth0,bridge=${bridge}" if [[ "${ipcfg}" == "dhcp" ]]; then net+=",ip=dhcp" else net+=",ip=${ipcfg}" fi if [[ -n "${vlan}" ]]; then net+=",tag=${vlan}" fi echo "${net}" } # Wait for DHCP IP pct_wait_for_ip() { local ctid="${1:?ctid}" local tries=60 while (( tries-- > 0 )); do # Prefer pct exec hostname -I; fallback to ip -4 addr local ip="" ip="$(pct exec "${ctid}" -- bash -lc "hostname -I 2>/dev/null | awk '{print \$1}'" 2>/dev/null || true)" if [[ -z "${ip}" ]]; then ip="$(pct exec "${ctid}" -- bash -lc "ip -4 -o addr show scope global | awk '{print \$4}' | cut -d/ -f1 | head -n1" 2>/dev/null || true)" fi if [[ -n "${ip}" ]]; then echo "${ip}" return 0 fi sleep 1 done return 1 } pct_exec() { local ctid="${1:?ctid}" shift # run inside CT pct exec "${ctid}" -- bash -lc "$*" } # --------------------------- # CTID strategy (your decision) # Use: (unix_time - 1_000_000_000) => safe until 2038 # --------------------------- pve_select_customer_ctid() { local now now="$(date +%s)" local ctid=$(( now - 1000000000 )) # Guard: must be positive integer (( ctid > 0 )) || die "Computed CTID is invalid: ${ctid}" echo "${ctid}" } # --------------------------- # Secrets (NO tr pipes) # --------------------------- gen_hex() { local nbytes="${1:-32}" # openssl rand -hex N -> 2N hex chars, no pipes, no 'tr' openssl rand -hex "${nbytes}" } # JSON escaping minimal for our known safe values (hex + simple strings) json_escape() { local s="$1" s="${s//\\/\\\\}" s="${s//\"/\\\"}" s="${s//$'\n'/\\n}" echo -n "$s" }