feat: implement update backend API and client logic
Add complete update mechanism for client-side updates: - backend/update.py: Core update logic (check, apply, rollback, status/logs) - backend/app.py: REST API endpoints (GET /update/status, POST /update/check, POST /update/apply, POST /update/rollback, GET /update/logs) - backend/models.py: Pydantic models for update API responses - backend/settings.py: Update config (status/log file paths) - scripts/rollback_client.sh: Rollback script for failed updates - scripts/update_client.sh: Enhanced update client script - CLAUDE.md: Documentation for future Claude Code instances Complete US_000026-028 and TASK_000027-029: - US_000026: Client pulls updates from remote service - US_000027: Client verifies and applies updates atomically - US_000028: Client reports update status to backend All endpoints require authentication. Updates run asynchronously. Documentation updated per SOP (CHANGELOG, PROJECT_STATUS, stories/tasks). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
76
scripts/rollback_client.sh
Executable file
76
scripts/rollback_client.sh
Executable file
@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SERVICE_NAME="${SERVICE_NAME:-skd}"
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/sk}"
|
||||
STATUS_FILE="${SKD_UPDATE_STATUS_FILE:-/var/lib/skd/update_status.json}"
|
||||
LOG_FILE="${SKD_UPDATE_LOG_FILE:-/var/lib/skd/update_logs.jsonl}"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||
}
|
||||
|
||||
write_status() {
|
||||
local status="$1"
|
||||
local error="${2:-}"
|
||||
local version="$3"
|
||||
SKD_STATUS="${status}" SKD_ERROR="${error}" SKD_VERSION="${version}" \
|
||||
SKD_STATUS_FILE="${STATUS_FILE}" SKD_LOG_FILE="${LOG_FILE}" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
status = os.environ["SKD_STATUS"]
|
||||
error = os.environ.get("SKD_ERROR", "")
|
||||
version = os.environ.get("SKD_VERSION", "unknown")
|
||||
status_file = Path(os.environ["SKD_STATUS_FILE"])
|
||||
log_file = Path(os.environ["SKD_LOG_FILE"])
|
||||
|
||||
status_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
payload = {
|
||||
"device_id": os.uname().nodename,
|
||||
"version": version,
|
||||
"status": status,
|
||||
"error": error,
|
||||
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
}
|
||||
status_file.write_text(json.dumps(payload), encoding="utf-8")
|
||||
with log_file.open("a", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(payload) + "\n")
|
||||
PY
|
||||
}
|
||||
|
||||
LATEST_BACKUP="$(ls -dt /opt/sk_backup_* 2>/dev/null | head -1 || true)"
|
||||
if [[ -z "${LATEST_BACKUP}" ]]; then
|
||||
log "No backup found; rollback aborted."
|
||||
write_status "failed" "no backup found" "unknown"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="unknown"
|
||||
if [[ -f "${LATEST_BACKUP}/VERSION" ]]; then
|
||||
VERSION="$(cat "${LATEST_BACKUP}/VERSION" | tr -d '\n')"
|
||||
fi
|
||||
|
||||
log "Stopping service ${SERVICE_NAME}..."
|
||||
sudo systemctl stop "${SERVICE_NAME}.service"
|
||||
|
||||
FAILED_DIR="${INSTALL_DIR}_failed_$(date +%s)"
|
||||
log "Swapping ${INSTALL_DIR} -> ${FAILED_DIR}..."
|
||||
if [[ -d "${INSTALL_DIR}" ]]; then
|
||||
sudo mv "${INSTALL_DIR}" "${FAILED_DIR}"
|
||||
fi
|
||||
sudo mv "${LATEST_BACKUP}" "${INSTALL_DIR}"
|
||||
|
||||
log "Starting service ${SERVICE_NAME}..."
|
||||
if sudo systemctl start "${SERVICE_NAME}.service"; then
|
||||
log "Rollback completed."
|
||||
write_status "success" "" "${VERSION}"
|
||||
else
|
||||
log "Rollback failed."
|
||||
write_status "failed" "service start failed" "${VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
@ -5,6 +5,8 @@ SERVICE_NAME="${SERVICE_NAME:-skd}"
|
||||
UPDATE_URL="${SKD_UPDATE_URL:-https://update.wlkns.org}"
|
||||
UPDATE_TOKEN="${SKD_UPDATE_TOKEN:-}"
|
||||
STATUS_URL="${SKD_UPDATE_STATUS_URL:-https://update.wlkns.org/status}"
|
||||
STATUS_FILE="${SKD_UPDATE_STATUS_FILE:-/var/lib/skd/update_status.json}"
|
||||
LOG_FILE="${SKD_UPDATE_LOG_FILE:-/var/lib/skd/update_logs.jsonl}"
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/sk}"
|
||||
STAGING_DIR=""
|
||||
MANIFEST_FILE=""
|
||||
@ -20,6 +22,35 @@ report_status() {
|
||||
local error="${2:-}"
|
||||
local timestamp
|
||||
timestamp="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
device_id = os.uname().nodename
|
||||
version = os.environ.get("SKD_VERSION", "unknown")
|
||||
status = os.environ.get("SKD_STATUS", "unknown")
|
||||
error = os.environ.get("SKD_ERROR", "")
|
||||
timestamp = os.environ.get("SKD_TIMESTAMP") or datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
status_file = Path(os.environ["SKD_STATUS_FILE"])
|
||||
log_file = Path(os.environ["SKD_LOG_FILE"])
|
||||
|
||||
status_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
payload = {
|
||||
"device_id": device_id,
|
||||
"version": version,
|
||||
"status": status,
|
||||
"error": error,
|
||||
"timestamp": timestamp,
|
||||
}
|
||||
|
||||
status_file.write_text(json.dumps(payload), encoding="utf-8")
|
||||
with log_file.open("a", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(payload) + "\n")
|
||||
PY
|
||||
local payload
|
||||
payload=$(cat <<JSON
|
||||
{"device_id":"$(hostname)","version":"${VERSION}","status":"${status}","error":"${error}","timestamp":"${timestamp}"}
|
||||
@ -99,7 +130,9 @@ log "Verifying checksum..."
|
||||
CALC_SHA="$(sha256sum "${ARTIFACT_FILE}" | awk '{print $1}')"
|
||||
if [[ "${CALC_SHA}" != "${ARTIFACT_SHA256}" ]]; then
|
||||
echo "Checksum mismatch: expected ${ARTIFACT_SHA256} got ${CALC_SHA}" >&2
|
||||
report_status "failed" "checksum mismatch"
|
||||
SKD_VERSION="${VERSION}" SKD_STATUS="failed" SKD_ERROR="checksum mismatch" \
|
||||
SKD_TIMESTAMP="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" SKD_STATUS_FILE="${STATUS_FILE}" \
|
||||
SKD_LOG_FILE="${LOG_FILE}" report_status "failed" "checksum mismatch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -121,7 +154,9 @@ STAGING_DIR=""
|
||||
log "Starting service ${SERVICE_NAME}..."
|
||||
if sudo systemctl start "${SERVICE_NAME}.service"; then
|
||||
log "Update applied successfully."
|
||||
report_status "success" ""
|
||||
SKD_VERSION="${VERSION}" SKD_STATUS="success" SKD_ERROR="" \
|
||||
SKD_TIMESTAMP="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" SKD_STATUS_FILE="${STATUS_FILE}" \
|
||||
SKD_LOG_FILE="${LOG_FILE}" report_status "success" ""
|
||||
else
|
||||
log "Service failed to start, rolling back..."
|
||||
sudo rm -rf "${INSTALL_DIR}"
|
||||
@ -129,6 +164,8 @@ else
|
||||
sudo mv "${BACKUP_DIR}" "${INSTALL_DIR}"
|
||||
fi
|
||||
sudo systemctl start "${SERVICE_NAME}.service" || true
|
||||
report_status "failed" "service start failed"
|
||||
SKD_VERSION="${VERSION}" SKD_STATUS="failed" SKD_ERROR="service start failed" \
|
||||
SKD_TIMESTAMP="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" SKD_STATUS_FILE="${STATUS_FILE}" \
|
||||
SKD_LOG_FILE="${LOG_FILE}" report_status "failed" "service start failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user