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.
110 lines
5.5 KiB
Bash
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.'
|