#!/usr/bin/env bash set -Eeuo pipefail log_ts() { date "+[%F %T]"; } info() { echo "$(log_ts) INFO: $*" >&2; } warn() { echo "$(log_ts) WARN: $*" >&2; } die() { echo "$(log_ts) ERROR: $*" >&2; exit 1; } setup_traps() { trap 'rc=$?; [[ $rc -ne 0 ]] && echo "$(log_ts) ERROR: Failed at line ${BASH_LINENO[0]}: ${BASH_COMMAND} (exit=$rc)" >&2; 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 s="$1" pvesm status | awk 'NR>1{print $1}' | grep -qx "$s" } pve_bridge_exists() { local b="$1" ip link show "$b" >/dev/null 2>&1 } # Return ONLY template path on stdout. Logs go to stderr. pve_template_ensure_debian12() { local storage="$1" local tmpl="debian-12-standard_12.12-1_amd64.tar.zst" local cache="/var/lib/vz/template/cache/${tmpl}" # pveam templates must be on "local" (dir storage), not on zfs local tstore="$storage" if ! pveam available -section system >/dev/null 2>&1; then warn "pveam not working? continuing" fi # heuristic: if storage isn't usable for templates, fallback to local # Most Proxmox setups use 'local' for templates. if ! pvesm status | awk 'NR>1{print $1,$2}' | grep -q "^${tstore} "; then warn "pveam storage '${tstore}' not found; falling back to 'local'" tstore="local" fi # If storage exists but isn't a dir storage for templates, pveam will fail -> fallback if ! pveam list "${tstore}" >/dev/null 2>&1; then warn "pveam storage '${tstore}' not available for templates; falling back to 'local'" tstore="local" fi if [[ ! -f "$cache" ]]; then info "Downloading CT template to ${tstore}: ${tmpl}" pveam download "${tstore}" "${tmpl}" >&2 fi echo "${tstore}:vztmpl/${tmpl}" } # Build net0 string (with optional vlan tag) pve_build_net0() { local bridge="$1" local ipcfg="$2" local vlan="${3:-0}" local mac mac="$(gen_mac)" local net="name=eth0,bridge=${bridge},hwaddr=${mac}" if [[ "$vlan" != "0" ]]; then net+=",tag=${vlan}" fi if [[ "$ipcfg" == "dhcp" ]]; then net+=",ip=dhcp" else net+=",ip=${ipcfg}" fi echo "$net" } # Wait for IP from pct; returns first IPv4 pct_wait_for_ip() { local ctid="$1" local i ip for i in $(seq 1 40); do 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)" 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 "$*" } # Push a text file into CT without SCP pct_push_text() { local ctid="$1" local dest="$2" local content="$3" pct exec "$ctid" -- bash -lc "cat > '$dest' <<'EOF' ${content} EOF" } # Cluster VMID existence check (best effort) # Uses pvesh cluster resources. If API not available, returns false (and caller can choose another approach). pve_vmid_exists_cluster() { local vmid="$1" pvesh get /cluster/resources --output-format json 2>/dev/null \ | python3 - <<'PY' "$vmid" || exit 0 import json,sys vmid=sys.argv[1] try: data=json.load(sys.stdin) except Exception: sys.exit(0) for r in data: if str(r.get("vmid",""))==str(vmid): sys.exit(1) sys.exit(0) PY [[ $? -eq 1 ]] } # Your agreed CTID scheme: unix time - 1,000,000,000 pve_ctid_from_unixtime() { local ts="$1" echo $(( ts - 1000000000 )) } # ----- Generators / policies ----- # Avoid "tr: Broken pipe" by not piping random through tr|head. gen_hex_64() { # 64 hex chars = 32 bytes openssl rand -hex 32 } gen_mac() { # locally administered unicast: 02:xx:xx:xx:xx:xx printf '02:%02x:%02x:%02x:%02x:%02x\n' \ "$((RANDOM%256))" "$((RANDOM%256))" "$((RANDOM%256))" "$((RANDOM%256))" "$((RANDOM%256))" } password_policy_check() { local p="$1" [[ ${#p} -ge 8 ]] || return 1 [[ "$p" =~ [0-9] ]] || return 1 [[ "$p" =~ [A-Z] ]] || return 1 return 0 } gen_password_policy() { # generate until it matches policy (no broken pipes, deterministic enough) local p while true; do # 18 chars, base64-ish but remove confusing chars p="$(openssl rand -base64 18 | tr -d '/+=' | cut -c1-16)" # ensure at least one uppercase and number p="${p}A1" password_policy_check "$p" && { echo "$p"; return 0; } done } emit_json() { # prints to stdout only; keep logs on stderr cat }