Files
kiddo/scripts/update_client.sh

190 lines
5.9 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SERVICE_NAME="${SERVICE_NAME:-skd}"
UPDATE_SERVICE_URL="${SKD_UPDATE_SERVICE_URL:-https://update.wlkns.org}"
PROJECT_ID="${SKD_UPDATE_PROJECT_ID:-safe-kiddo-control}"
UPDATE_TOKEN="${SKD_UPDATE_TOKEN:-}"
MANIFEST_URL="${UPDATE_SERVICE_URL}/v1/projects/${PROJECT_ID}/manifest"
STATUS_URL="${UPDATE_SERVICE_URL}/v1/projects/${PROJECT_ID}/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=""
ARTIFACT_FILE=""
BACKUP_DIR=""
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
report_status() {
local status="$1"
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
{
"project_id": "${PROJECT_ID}",
"version": "${VERSION}",
"status": "${status}",
"timestamp": "${timestamp}",
"client_id": "$(hostname)",
"error_code": "${error}",
"reason": "${error}"
}
JSON
)
if [[ -n "${UPDATE_TOKEN}" ]]; then
curl -sS -X POST -H "Authorization: Bearer ${UPDATE_TOKEN}" -H "Content-Type: application/json" \
-d "${payload}" "${STATUS_URL}" >/dev/null || true
else
curl -sS -X POST -H "Content-Type: application/json" -d "${payload}" "${STATUS_URL}" >/dev/null || true
fi
}
cleanup() {
if [[ -n "${STAGING_DIR}" && -d "${STAGING_DIR}" ]]; then
rm -rf "${STAGING_DIR}"
fi
if [[ -n "${MANIFEST_FILE}" && -f "${MANIFEST_FILE}" ]]; then
rm -f "${MANIFEST_FILE}"
fi
if [[ -n "${ARTIFACT_FILE}" && -f "${ARTIFACT_FILE}" ]]; then
rm -f "${ARTIFACT_FILE}"
fi
}
trap cleanup EXIT
error_exit() {
local msg="${1:-update failed}"
SKD_VERSION="${VERSION:-unknown}" SKD_STATUS="failed" SKD_ERROR="${msg}" \
SKD_TIMESTAMP="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" SKD_STATUS_FILE="${STATUS_FILE}" \
SKD_LOG_FILE="${LOG_FILE}" report_status "failed" "${msg}"
}
trap 'error_exit "update failed at line ${LINENO}"' ERR
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Required command not found: $1" >&2
exit 1
fi
}
require_cmd curl
require_cmd tar
require_cmd sha256sum
require_cmd python3
log "Fetching update manifest from ${MANIFEST_URL}..."
MANIFEST_FILE="$(mktemp)"
if [[ -n "${UPDATE_TOKEN}" ]]; then
curl -fsS -H "Authorization: Bearer ${UPDATE_TOKEN}" "${MANIFEST_URL}" -o "${MANIFEST_FILE}"
else
curl -fsS "${MANIFEST_URL}" -o "${MANIFEST_FILE}"
fi
read_manifest() {
python3 - <<'PY' "${MANIFEST_FILE}"
import json
import sys
path = sys.argv[1]
with open(path, "r", encoding="utf-8") as fh:
data = json.load(fh)
for key in ("version", "artifact_url", "sha256"):
if key not in data:
sys.exit(f"Manifest missing '{key}'")
print(data["version"])
print(data["artifact_url"])
print(data["sha256"])
PY
}
readarray -t manifest_values < <(read_manifest)
VERSION="${manifest_values[0]}"
ARTIFACT_URL="${manifest_values[1]}"
ARTIFACT_SHA256="${manifest_values[2]}"
log "Downloading artifact ${ARTIFACT_URL} (version ${VERSION})..."
ARTIFACT_FILE="$(mktemp --suffix=.tar.gz)"
if [[ -n "${UPDATE_TOKEN}" ]]; then
curl -fsS -H "Authorization: Bearer ${UPDATE_TOKEN}" "${ARTIFACT_URL}" -o "${ARTIFACT_FILE}"
else
curl -fsS "${ARTIFACT_URL}" -o "${ARTIFACT_FILE}"
fi
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
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
STAGING_DIR="$(mktemp -d /opt/sk_update.XXXXXX)"
log "Extracting to staging ${STAGING_DIR}..."
tar -xzf "${ARTIFACT_FILE}" -C "${STAGING_DIR}"
BACKUP_DIR="/opt/sk_backup_${VERSION}_$(date +%s)"
log "Stopping service ${SERVICE_NAME}..."
sudo systemctl stop "${SERVICE_NAME}.service"
log "Swapping ${INSTALL_DIR} -> ${BACKUP_DIR}..."
if [[ -d "${INSTALL_DIR}" ]]; then
sudo mv "${INSTALL_DIR}" "${BACKUP_DIR}"
fi
sudo mv "${STAGING_DIR}" "${INSTALL_DIR}"
STAGING_DIR=""
log "Starting service ${SERVICE_NAME}..."
if sudo systemctl start "${SERVICE_NAME}.service"; then
log "Update applied successfully."
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}"
if [[ -d "${BACKUP_DIR}" ]]; then
sudo mv "${BACKUP_DIR}" "${INSTALL_DIR}"
fi
sudo systemctl start "${SERVICE_NAME}.service" || true
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