#!/usr/bin/env bash set -Eeuo pipefail # --------------------------- # Logging / trap helpers # --------------------------- _ts() { date '+%F %T'; } _is_tty() { [[ -t 1 ]]; } _color() { local code="$1"; shift if _is_tty; then printf "\033[%sm%s\033[0m" "$code" "$*"; else printf "%s" "$*"; fi } log() { echo "[$(_ts)] $*"; } info() { log "$(_color '36' 'INFO:') $*"; } warn() { log "$(_color '33' 'WARN:') $*"; } err() { log "$(_color '31' 'ERROR:') $*"; } die() { err "$*"; exit 1; } 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 } 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" ]] } # Ensure Debian 12 template. Return "storage:vztmpl/xxx.tar.zst" pve_template_ensure_debian12() { local store="$1" local tpl="debian-12-standard_12.12-1_amd64.tar.zst" need_cmd pveam awk grep # Some storages (like 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 pveam update >/dev/null # If not available locally, download if ! pveam list "$store" | 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}" } # Build net0 string with optional VLAN tag pve_build_net0() { local bridge="$1" local ip="$2" local vlan="${3:-}" if [[ "$ip" == "dhcp" ]]; then if [[ -n "$vlan" ]]; then echo "name=eth0,bridge=${bridge},ip=dhcp,tag=${vlan}" else echo "name=eth0,bridge=${bridge},ip=dhcp" fi else # static CIDR if [[ -n "$vlan" ]]; then echo "name=eth0,bridge=${bridge},ip=${ip},tag=${vlan}" else echo "name=eth0,bridge=${bridge},ip=${ip}" fi fi } pct_exec() { local ctid="$1"; shift # run a command inside CT with bash -lc pct exec "$ctid" -- bash -lc "$*" } # Wait until CT has an IPv4 on eth0. Returns IP or empty. pct_wait_for_ip() { local ctid="$1" local tries="${2:-60}" local delay="${3:-1}" local ip="" while (( tries > 0 )); 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 "$delay" ((tries--)) done echo "" return 1 } # --------------------------- # CTID strategy (your request) # --------------------------- # Customer-safe CTID: # CTID = (unix_time - 1000000000) # This is stable until 2038 and avoids collisions with typical low VMIDs. customer_ctid_from_time() { local now now="$(date +%s)" echo $(( now - 1000000000 )) } # Check if a VMID exists cluster-wide using pvesh. # If pvesh fails (permissions/API), we still have a fallback. pve_vmid_exists_cluster() { local vmid="$1" need_cmd pvesh python3 # pvesh returns JSON; we parse safely in python local json json="$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null || true)" [[ -n "$json" ]] || return 1 python3 - "$vmid" <<'PY' 2>/dev/null import sys, json vmid = int(sys.argv[1]) data = json.load(sys.stdin) for r in data: if int(r.get("vmid", -1)) == vmid: sys.exit(0) sys.exit(1) PY } # Select a CTID that does NOT exist cluster-wide. pve_select_customer_ctid() { local ctid ctid="$(customer_ctid_from_time)" # If cluster check works, avoid collisions. if pve_vmid_exists_cluster "$ctid"; then # extremely unlikely, but add +1..+60 tries local i for i in $(seq 1 60); do if ! pve_vmid_exists_cluster "$((ctid+i))"; then echo "$((ctid+i))" return 0 fi done die "CTID selection failed: all candidates busy" fi echo "$ctid" } # --------------------------- # Secrets (no 'tr: broken pipe') # --------------------------- _rand_hex() { local nbytes="${1:-32}" if command -v openssl >/dev/null 2>&1; then openssl rand -hex "$nbytes" return 0 fi if command -v python3 >/dev/null 2>&1; then python3 - </dev/null 2>&1; then python3 - <