Files
praxis/scripts/proxmox/api.sh
T
Praxis CI bb17615f41 feat(P01): SLICE-03+04 — PVE API layer (8 scripts) + health-check
SLICE-03 (devops-engineer): ported from coreci/scripts/proxmox/:
  api.sh (verbatim), ct-exists.sh, lxc-clone.sh (4GB/16GB, hostname=praxis),
  lxc-config.sh (praxis env vars, praxis-firstboot.sh snippet), lxc-start.sh,
  stage-snippet.sh (G-101 FIX: bakes GITEA_TOKEN into snippet via sed),
  timing.sh (verbatim), rollback.sh (proxy code removed)
SLICE-04 (devops-engineer): health-check.sh adapted for /health:8789
  (G-104 FIX: timeout bumped 300s→600s for Docker build margin)

REQ-DEPLOY-03, 04, 05, 07, 08 covered.

---ci---
project: praxis
phase: 1
milestone: v0.2
status: execute
slice: 03-04
wave: 2
---/ci---
2026-08-01 14:18:23 +00:00

175 lines
6.7 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 <method> <path> [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 <upid>
# 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
}