bb17615f41
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---
175 lines
6.7 KiB
Bash
Executable File
175 lines
6.7 KiB
Bash
Executable File
#!/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
|
||
} |