6cf63cb064
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---
176 lines
6.9 KiB
Bash
Executable File
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" |