Files
praxis/scripts/proxmox/lxc-deploy.sh
T
Praxis CI 6cf63cb064 docs(P01): verify — APPROVE_WITH_NOTES, 4 P0 fixed, 18/20 REQ covered
Verification layers:
  Structural: PASS (all scripts executable, syntax clean, Dockerfile valid)
  Behavioral: PASS (121 bats, 77 pytest, docker build succeeds, compose valid)
  Security: PASS (no secrets committed, .dockerignore excludes .env*, env_file pattern)
  Quality: PASS (coreci patterns followed, no coreci refs, G-104/G-105/G-106 verified)

P0 issues found and auto-fixed:
  1. docker-compose.yml: removed invalid restart_policy key, fixed env_file syntax
  2. pyproject.toml: added fastapi + uvicorn deps (v0.1 gap exposed by Dockerfile)
  3. timing.sh: renamed coreci_deploy_timing → praxis_deploy_timing (TASK-03-07)
  4. firstboot-hook.sh: fixed idempotency check (/opt/praxis/.git not /usr/local/bin/praxis-deploy)

P1+ issues: 8 (1 fixed: lxc-config.sh default alignment, 7 noted for post-hoc review)
REQ coverage: 18/20 covered, 2 deferred (live first-boot timing + live E2E require cluster)
Must-haves: 25/28 pass, 2 partial (comment-only diffs, no Makefile), 1 deferred

---ci---
project: praxis
phase: 1
milestone: v0.2
status: verify
---/ci---
2026-08-03 18:37:45 +00:00

176 lines
6.9 KiB
Bash
Executable File

#!/bin/sh
# Praxis — Orchestrator: deploy praxis to a Proxmox LXC container.
#
# Adapted from coreci/scripts/proxmox/lxc-deploy.sh.
# Sequence: stage snippet → clone template → configure CT → start →
# health-check → rollback on failure.
#
# Required env (see .env.example + ~/coreci/.ciagent/.env.secrets):
# PROXMOX_API_URL — https://proxmox:8006/api2/json
# PROXMOX_API_TOKEN — USER@REALM!TOKENID=SECRET
# PROXMOX_NODE — target node name
# PROXMOX_STORAGE — storage holding the template
# PROXMOX_TEMPLATE_VOLID — local:vztmpl/debian-12-template.tar.zst
# GITEA_TOKEN — bearer token for the private Gitea repo
# (baked into the firstboot snippet by stage-snippet.sh)
#
# Optional env:
# PROXMOX_LXC_VMID — target CT VMID (default: auto-allocate via pve_nextid)
# PRAXIS_VERSION — git ref to deploy (default: main)
# PRAXIS_PORT — server HTTP port (default: 8789)
# PRAXIS_HEALTH_URL — override health-check URL
# PROXMOX_MEMORY_MB — CT memory limit (default: 4096)
# PROXMOX_TLS_SKIP_VERIFY— accept self-signed certs (default: false)
# DEEPGRAM_API_KEY — voice-service key (optional, may be empty)
# CARTESIA_API_KEY — voice-service key (optional, may be empty)
# OLLAMA_API_KEY — voice-service key (optional, may be empty)
#
# Flags:
# --recreate — rollback.sh (stop + destroy) then full redeploy
# --reconfigure — re-PUT lxc-config.sh + restart (no clone)
#
# Exit: 0 on successful deploy, 1 on failure (with rollback attempted)
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJ_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
# shellcheck source=api.sh disable=SC1091
. "${SCRIPT_DIR}/api.sh"
# shellcheck source=ct-exists.sh disable=SC1091
. "${SCRIPT_DIR}/ct-exists.sh"
# shellcheck source=timing.sh disable=SC1091
. "${SCRIPT_DIR}/timing.sh"
# ── Source secrets (D-026, MH-23) ──────────────────────────────────
# Proxmox secrets come from ~/coreci/.ciagent/.env.secrets (same cluster,
# same operator). Praxis secrets (GITEA_TOKEN, DEEPGRAM_API_KEY) come from
# praxis's own .ciagent/.env.secrets. Missing files emit a warning (the
# vars may already be in the environment from the CI runner); pve_env
# below fails fast if required vars are still unset.
CORECI_SECRETS="${HOME}/coreci/.ciagent/.env.secrets"
PRAXIS_SECRETS="${PROJ_ROOT}/.ciagent/.env.secrets"
if [ -f "$CORECI_SECRETS" ]; then
# shellcheck source=/dev/null disable=SC1091
. "$CORECI_SECRETS"
else
echo "deploy: WARNING — ${CORECI_SECRETS} not found (PROXMOX_* vars must be in env)" >&2
fi
if [ -f "$PRAXIS_SECRETS" ]; then
# shellcheck source=/dev/null disable=SC1091
. "$PRAXIS_SECRETS"
else
echo "deploy: WARNING — ${PRAXIS_SECRETS} not found (GITEA_TOKEN/DEEPGRAM_API_KEY must be in env)" >&2
fi
pve_env PROXMOX_API_URL PROXMOX_API_TOKEN PROXMOX_NODE \
PROXMOX_STORAGE PROXMOX_TEMPLATE_VOLID GITEA_TOKEN
# ── Flag parsing ───────────────────────────────────────────────────
recreate=0
reconfigure=0
for arg in "$@"; do
case "$arg" in
--recreate) recreate=1 ;;
--reconfigure) reconfigure=1 ;;
*) echo "deploy: unknown argument: $arg" >&2; exit 2 ;;
esac
done
# Step 0: stage the first-boot hookscript to Proxmox snippet storage.
# G-101 FIX: stage-snippet.sh bakes GITEA_TOKEN into the snippet.
hookscript_volid="${PROXMOX_STORAGE:-local}:snippets/praxis-firstboot.sh"
existing=$(pve_get "/nodes/${PROXMOX_NODE}/storage/${PROXMOX_STORAGE:-local}/content" 2>/dev/null | jq -r --arg v "$hookscript_volid" '.[]? | select(.volid==$v) | .volid' 2>/dev/null || true)
if [ -n "$existing" ]; then
echo "deploy: hookscript snippet ${hookscript_volid} already staged — skipping upload" >&2
else
"${SCRIPT_DIR}/stage-snippet.sh"
fi
# Resolve target VMID (D-027: auto-allocate by default).
vmid="${PROXMOX_LXC_VMID:-auto}"
if [ "$vmid" = "auto" ]; then
vmid=$(pve_nextid)
echo "deploy: auto-allocated VMID ${vmid}" >&2
else
echo "deploy: using configured VMID ${vmid}" >&2
fi
# Trap: rollback on any failure (mirrors coreci pattern).
deploy_failed=0
skip_rollback=0
trap 'deploy_failed=1' INT TERM
cleanup() {
rc=$?
if [ "$skip_rollback" -ne 1 ] && { [ "$deploy_failed" -ne 0 ] || [ "$rc" -ne 0 ]; }; then
echo "deploy: FAILED (rc=${rc}) — rolling back VMID ${vmid}" >&2
"${SCRIPT_DIR}/rollback.sh" "$vmid" 2>&1 || true
fi
}
trap cleanup EXIT
# ── Idempotency: detect existing CT before clone ──────────────────
if ct_exists "$vmid"; then
echo "deploy: VMID ${vmid} already exists — checking health" >&2
ct_healthy=0
if ct_running "$vmid"; then
if PRAXIS_HEALTH_TIMEOUT="${IDEMPOTENCY_HEALTH_TIMEOUT:-30}" \
"${SCRIPT_DIR}/health-check.sh" "$vmid" 2>/dev/null; then
ct_healthy=1
fi
fi
if [ "$ct_healthy" -eq 1 ]; then
echo "deploy: VMID ${vmid} already running + healthy — skipping clone/config/start (idempotent re-deploy)" >&2
skip_provision=1
elif [ "$reconfigure" -eq 1 ]; then
echo "deploy: VMID ${vmid} exists but unhealthy — --reconfigure: re-PUT config + restart" >&2
skip_rollback=1
timing_start reconfigure
"${SCRIPT_DIR}/lxc-config.sh" "$vmid"
"${SCRIPT_DIR}/lxc-start.sh" "$vmid"
timing_end reconfigure
timing_start health
"${SCRIPT_DIR}/health-check.sh" "$vmid"
timing_end health
skip_provision=1
elif [ "$recreate" -eq 1 ]; then
echo "deploy: VMID ${vmid} exists but unhealthy — --recreate: rollback + redeploy" >&2
"${SCRIPT_DIR}/rollback.sh" "$vmid"
skip_provision=0
else
echo "deploy: ERROR — VMID ${vmid} exists but is unhealthy." >&2
echo "deploy: Use --recreate to rollback + redeploy, or --reconfigure to update config + restart." >&2
echo "deploy: No action taken (the existing CT was left intact for inspection)." >&2
skip_rollback=1
exit 1
fi
else
skip_provision=0
fi
if [ "${skip_provision:-0}" -eq 0 ]; then
# Step 1: Clone the template
timing_start clone
"${SCRIPT_DIR}/lxc-clone.sh" "$vmid"
timing_end clone
# Step 2: Configure the CT
timing_start config
"${SCRIPT_DIR}/lxc-config.sh" "$vmid"
timing_end config
# Step 3: Start the CT
timing_start start
"${SCRIPT_DIR}/lxc-start.sh" "$vmid"
timing_end start
# Step 4: Health-check (G-104: 600s timeout for Docker build)
timing_start health
"${SCRIPT_DIR}/health-check.sh" "$vmid"
timing_end health
fi
deploy_failed=0
echo "deploy: praxis deployed successfully to VMID ${vmid}" >&2
printf 'VMID=%s\n' "$vmid"