#!/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 <&2; exit 1; } echo 'Electric Cherry host agent installed and enrolled. Host readiness is shown in Hosts.'