#!/bin/sh # CoreCI — Proxmox VE REST API shared helpers. # # Sourced by the other scripts/proxmox/*.sh scripts. Provides: # pve_curl — authenticated curl wrapper (PVEAPIToken header, TLS opt) # pve_poll — poll an async UPID until status == "stopped" # pve_nextid — fetch the next free VMID # pve_get — GET with 503 bounded retry (idempotent reads only) # pve_env — validate required env vars are set # # All helpers use `set -eu` semantics (fail fast). The caller is # expected to `set -eu` and `source` this file. # ── TLS handling ────────────────────────────────────────────── # PROXMOX_TLS_SKIP_VERIFY=true → curl --insecure (self-signed certs). # Default is false (secure; operator opts in for self-signed). pve_tls_insecure() { case "${PROXMOX_TLS_SKIP_VERIFY:-false}" in true|1|yes|TRUE) echo "--insecure" ;; *) echo "" ;; esac } # ── Auth header ──────────────────────────────────────────────── # PVEAPIToken=USER@REALM!TOKENID=SECRET (no ticket step, no CSRF) pve_auth_header() { printf '%s' "PVEAPIToken=${PROXMOX_API_TOKEN:?PROXMOX_API_TOKEN is required}" } # ── Core curl wrapper ────────────────────────────────────────── # Usage: pve_curl [form-data-args...] # Returns the raw JSON `data` field on stdout (jq -r .data). # Exits non-zero on HTTP >= 300 or curl failure. pve_curl() { method="$1"; path="$2"; shift 2 url="${PROXMOX_API_URL:?PROXMOX_API_URL is required}${path}" insecure="$(pve_tls_insecure)" if [ "$#" -gt 0 ]; then # Form-encoded body for POST/PUT (key=value pairs) data_args="" for pair in "$@"; do data_args="${data_args} --data-urlencode ${pair}" done # shellcheck disable=SC2086 response=$(curl -sS $insecure \ -X "$method" \ -H "Authorization: $(pve_auth_header)" \ -H "Content-Type: application/x-www-form-urlencoded" \ $data_args \ "$url") else # shellcheck disable=SC2086 response=$(curl -sS $insecure \ -X "$method" \ -H "Authorization: $(pve_auth_header)" \ "$url") fi # Proxmox always wraps responses in {"data": ...}. Check for errors. status=$(printf '%s' "$response" | jq -r '.errors // empty') if [ -n "$status" ]; then echo "pve_curl: API error for $method $path: $status" >&2 printf '%s' "$response" >&2 return 1 fi printf '%s' "$response" | jq -r '.data' } # ── GET with 503 bounded retry (idempotent reads only) ──────── # IDEATE-19: transient 503s (node busy/restarting) retried 3× / 2s backoff. # NOT used for mutating calls (clone/start/stop) — those are UPID-polled. pve_get() { path="$1" url="${PROXMOX_API_URL:?}${path}" insecure="$(pve_tls_insecure)" attempt=0 max=3 while [ "$attempt" -lt "$max" ]; do # shellcheck disable=SC2086 response=$(curl -sS -w '\n%{http_code}' $insecure \ -X GET \ -H "Authorization: $(pve_auth_header)" \ "$url") http_code=$(printf '%s' "$response" | tail -1) body=$(printf '%s' "$response" | sed '$d') if [ "$http_code" = "503" ] && [ "$((attempt + 1))" -lt "$max" ]; then attempt=$((attempt + 1)) echo "pve_get: 503 from $path, retry $attempt/$max in 2s..." >&2 sleep 2 continue fi if [ "$http_code" != "200" ]; then echo "pve_get: HTTP $http_code for $path" >&2 printf '%s' "$body" >&2 return 1 fi printf '%s' "$body" | jq -r '.data' return 0 done # Exhausted all 503 retries. echo "pve_get: 503 from $path after $max attempts" >&2 return 1 } # ── UPID polling ─────────────────────────────────────────────── # Mutating Proxmox calls return a UPID string. Poll until done. # Usage: pve_poll # Exits non-zero if the task exitstatus != "OK". pve_poll() { upid="$1" node="${PROXMOX_NODE:?PROXMOX_NODE is required}" path="/nodes/${node}/tasks/${upid}/status" attempt=0 max_attempts=120 # 120 × 2s = 4 min max while [ "$attempt" -lt "$max_attempts" ]; do status=$(pve_curl GET "$path") running=$(printf '%s' "$status" | jq -r '.status') if [ "$running" = "stopped" ]; then exitstatus=$(printf '%s' "$status" | jq -r '.exitstatus') # "OK" is the clean success. "WARNINGS: N" is a successful # completion with non-fatal warnings (e.g. systemd 255 # nesting hint on CT create). Both are acceptable. case "$exitstatus" in OK|WARNINGS\ *) return 0 ;; *) echo "pve_poll: task $upid failed with exitstatus: $exitstatus" >&2 return 1 ;; esac fi attempt=$((attempt + 1)) sleep 2 done echo "pve_poll: timeout waiting for task $upid" >&2 return 1 } # ── Next free VMID ──────────────────────────────────────────── pve_nextid() { pve_curl GET "/cluster/nextid" | jq -r '. | tonumber' } # ── Env validation ──────────────────────────────────────────── # Usage: pve_env VAR1 VAR2 ... — exits 1 if any is unset/empty pve_env() { missing=0 for var in "$@"; do eval "val=\"\${${var}:-}\"" if [ -z "$val" ]; then echo "pve_env: $var is required but not set" >&2 missing=1 fi done return "$missing" } # ── lxc.environment form-encoding helper ────────────────────── # Proxmox PUT /config accepts repeated lxc.environment=KEY=value. # This builds the curl data args from KEY=value pairs. # Usage: pve_lxc_env_args KEY1=VAL1 KEY2=VAL2 ... # Emits one "lxc.environment=KEY=VAL" token per arg, newline-separated, # so the caller can pass each line to curl --data-urlencode. (Prior # version concatenated all args into a single malformed blob.) pve_lxc_env_args() { first=1 for pair in "$@"; do [ "$first" -eq 0 ] && printf '\n' printf '%s' "lxc.environment=${pair}" first=0 done }