Files
electric-cherry/static/agent/install.sh
T
josh e5c0fda1ed Add native host management and role-based operator authentication
Introduce an outbound Python host agent, Debian installer, durable remote
operations, immutable game-file libraries, host-aware deployments and ports,
and occupancy-safe campaigns and draining. Native hosts do not require Komodo.

Add administrator, operator and viewer accounts, protected management routes,
secure sessions, password recovery, audit history and separate telemetry tokens.
Document first-admin setup, reporter migration and live-host acceptance checks.

Validation: 1,343 TypeScript tests and 39 agent tests passed, along with type
checking, the production build, compiled SSR/API smoke tests and administrator
recovery. Real Debian/Docker T5/T6 acceptance remains unrun.
2026-10-03 11:42:36 -04:00

110 lines
5.5 KiB
Bash

#!/bin/bash
# Electric Cherry host installer. Downloaded release integrity is checked before
# execution. Existing Docker data and unrelated containers are never changed.
set -euo pipefail
umask 077
controller=''
enrollment_token=''
data_dir='/var/lib/electric-cherry'
reenroll=''
while [ "$#" -gt 0 ]; do
case "$1" in
--controller) controller="${2:?Missing controller URL}"; shift 2 ;;
--token) enrollment_token="${2:?Missing enrollment token}"; shift 2 ;;
--data-dir) data_dir="${2:?Missing data directory}"; shift 2 ;;
--reenroll) reenroll='--force-enroll'; shift ;;
*) echo "Unknown installer option" >&2; exit 2 ;;
esac
done
[ "$(id -u)" = 0 ] || { echo 'Run the installer as root.' >&2; exit 1; }
case "$controller" in https://*) ;; *) echo '--controller must be an HTTPS URL.' >&2; exit 2 ;; esac
[ -n "$enrollment_token" ] || { echo '--token is required.' >&2; exit 2; }
case "$data_dir" in /*) ;; *) echo '--data-dir must be absolute.' >&2; exit 2 ;; esac
case "$data_dir" in *[!a-zA-Z0-9/_.-]*|/|/etc|/usr|/var|/var/lib) echo 'Choose a dedicated data directory.' >&2; exit 2 ;; esac
[ ! -L "$data_dir" ] || { echo 'Data directory must not be a symlink.' >&2; exit 2; }
. /etc/os-release
[ "${ID:-}" = debian ] || { echo 'Supported platform: Debian 12 or 13.' >&2; exit 1; }
case "${VERSION_ID:-}" in 12|13) ;; *) echo 'Supported platform: Debian 12 or 13.' >&2; exit 1 ;; esac
[ "$(dpkg --print-architecture)" = amd64 ] || { echo 'Supported architecture: amd64.' >&2; exit 1; }
command -v systemctl >/dev/null || { echo 'systemd is required.' >&2; exit 1; }
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y --no-install-recommends ca-certificates curl python3
python3 - "$controller" "$data_dir" <<'PY'
import json, pathlib, sys
config = pathlib.Path('/etc/electric-cherry/agent.json')
if config.exists():
saved = json.loads(config.read_text())
if saved.get('controller', '').rstrip('/') != sys.argv[1].rstrip('/') or pathlib.Path(saved.get('root', '')).resolve() != pathlib.Path(sys.argv[2]).resolve():
raise SystemExit('Existing enrollment uses a different controller or data directory. Preserve the existing installation and migrate it explicitly.')
PY
if ! command -v docker >/dev/null; then
if [ -e /var/lib/docker ] || [ -e /etc/docker/daemon.json ]; then
echo 'Existing Docker data/configuration detected without a Docker CLI. Repair Docker, then rerun.' >&2
exit 1
fi
apt-get install -y --no-install-recommends docker.io
systemctl enable --now docker
fi
docker info >/dev/null 2>&1 || { echo 'Docker is installed but unavailable. Start or repair it, then rerun.' >&2; exit 1; }
install -d -m 0755 "$data_dir" "$data_dir/libraries" "$data_dir/instances"
install -d -m 0700 "$data_dir/agent" /etc/electric-cherry
available_kb=$(df -Pk "$data_dir" | awk 'NR==2 {print $4}')
[ "$available_kb" -ge 1048576 ] || { echo 'At least 1 GiB of available game storage is required.' >&2; exit 1; }
temporary_dir=$(mktemp -d)
trap 'rm -rf "$temporary_dir"' EXIT
controller="${controller%/}"
curl --proto '=https' --tlsv1.2 --fail --silent --show-error "$controller/api/agent/release/manifest.json" -o "$temporary_dir/manifest.json"
curl --proto '=https' --tlsv1.2 --fail --silent --show-error "$controller/api/agent/release/ec-agent.py" -o "$temporary_dir/ec-agent.py"
python3 - "$temporary_dir/manifest.json" "$temporary_dir/ec-agent.py" <<'PY'
import hashlib, json, pathlib, sys
manifest = json.loads(pathlib.Path(sys.argv[1]).read_text())
actual = hashlib.sha256(pathlib.Path(sys.argv[2]).read_bytes()).hexdigest()
if actual != manifest.get('sha256'):
raise SystemExit('Agent release checksum verification failed.')
if manifest.get('platform') != 'debian-amd64' or manifest.get('protocolVersion') != 1:
raise SystemExit('Unsupported agent release manifest.')
PY
# Stop only the agent while replacing its own executable/journal owner. Existing
# game containers keep running under Docker throughout installation and updates.
if systemctl cat electric-cherry-agent.service >/dev/null 2>&1; then
systemctl stop electric-cherry-agent.service
fi
install -m 0755 "$temporary_dir/ec-agent.py" /usr/local/libexec/electric-cherry-agent.py 2>/dev/null || {
install -d -m 0755 /usr/local/libexec
install -m 0755 "$temporary_dir/ec-agent.py" /usr/local/libexec/electric-cherry-agent.py
}
printf '%s' "$enrollment_token" > "$temporary_dir/enrollment-token"
unset enrollment_token
python3 /usr/local/libexec/electric-cherry-agent.py --enroll --controller "$controller" --root "$data_dir" --token-file "$temporary_dir/enrollment-token" ${reenroll:+"$reenroll"}
cat > /etc/systemd/system/electric-cherry-agent.service <<EOF
[Unit]
Description=Electric Cherry Host Agent
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service
[Service]
Type=simple
ExecStart=/usr/bin/python3 /usr/local/libexec/electric-cherry-agent.py --config /etc/electric-cherry/agent.json
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
UMask=0077
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=$data_dir /etc/electric-cherry
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable electric-cherry-agent
systemctl restart electric-cherry-agent
systemctl is-active --quiet electric-cherry-agent || { echo 'Agent service did not start. Inspect journalctl -u electric-cherry-agent.' >&2; exit 1; }
echo 'Electric Cherry host agent installed and enrolled. Host readiness is shown in Hosts.'