#!/usr/bin/env bash set -euo pipefail SERVICE_NAME="aether-gateway" SERVICE_USER="${SERVICE_USER:-aether}" SERVICE_GROUP="${SERVICE_GROUP:-aether}" INSTALL_ROOT="${INSTALL_ROOT:-/opt/aether}" CONFIG_DIR="${CONFIG_DIR:-/etc/aether}" ENV_TARGET="${CONFIG_DIR}/aether-gateway.env" SYSTEMD_UNIT_PATH="/etc/systemd/system/${SERVICE_NAME}.service" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)" BIN_SOURCE="${REPO_ROOT}/target/release/aether-gateway" FRONTEND_SOURCE="${REPO_ROOT}/frontend/dist" SERVICE_SOURCE="${REPO_ROOT}/deploy/systemd/aether-gateway.service" ENV_EXAMPLE_SOURCE="${REPO_ROOT}/deploy/systemd/aether-gateway.env.example" DATA_COMPOSE_SOURCE="${REPO_ROOT}/deploy/docker-compose.data.yml" ENV_SOURCE="" RELEASE_ID="${RELEASE_ID:-$(date +%Y%m%d%H%M%S)}" SKIP_START="false" usage() { cat <<'EOF' Usage: sudo deploy/systemd/install-systemd.sh [options] Options: --env-file PATH Copy PATH to /etc/aether/aether-gateway.env before install --release-id ID Release identifier under /opt/aether/releases/ (default: timestamp) --bin-source PATH Built aether-gateway binary (default: target/release/aether-gateway) --frontend-source PATH Built frontend directory (default: frontend/dist) --skip-start Install files and unit, but do not restart the service -h, --help Show this help EOF } die() { echo "ERROR: $*" >&2 exit 1 } info() { echo ">>> $*" } warn() { echo "WARNING: $*" >&2 } require_root() { if [[ "${EUID}" -ne 0 ]]; then die "run as root" fi } require_linux_systemd() { [[ "$(uname -s)" == "Linux" ]] || die "systemd deployment is only supported on Linux" command -v systemctl >/dev/null 2>&1 || die "systemctl not found" } trim_whitespace() { local value="$1" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" printf '%s' "${value}" } strip_optional_quotes() { local value="$1" if [[ ${#value} -ge 2 ]]; then if [[ "${value:0:1}" == "\"" && "${value: -1}" == "\"" ]]; then value="${value:1:${#value}-2}" elif [[ "${value:0:1}" == "'" && "${value: -1}" == "'" ]]; then value="${value:1:${#value}-2}" fi fi printf '%s' "${value}" } is_placeholder_value() { local value="$1" case "${value}" in *change-me*|*change-this*|*your_secure_password_here*|*your_redis_password_here*) return 0 ;; *) return 1 ;; esac } find_nologin_shell() { if [[ -x /usr/sbin/nologin ]]; then echo "/usr/sbin/nologin" elif [[ -x /sbin/nologin ]]; then echo "/sbin/nologin" else echo "/bin/false" fi } ensure_service_account() { if ! getent group "${SERVICE_GROUP}" >/dev/null 2>&1; then info "creating group ${SERVICE_GROUP}" groupadd --system "${SERVICE_GROUP}" fi if ! id -u "${SERVICE_USER}" >/dev/null 2>&1; then info "creating user ${SERVICE_USER}" useradd \ --system \ --gid "${SERVICE_GROUP}" \ --home-dir "${INSTALL_ROOT}" \ --shell "$(find_nologin_shell)" \ "${SERVICE_USER}" fi } ensure_sources_exist() { [[ -x "${BIN_SOURCE}" ]] || die "binary not found or not executable: ${BIN_SOURCE}. Run: cargo build --release -p aether-gateway" [[ -d "${FRONTEND_SOURCE}" ]] || die "frontend dist not found: ${FRONTEND_SOURCE}. Run: (cd frontend && npm ci && npm run build)" [[ -f "${SERVICE_SOURCE}" ]] || die "service unit template not found: ${SERVICE_SOURCE}" [[ -f "${ENV_EXAMPLE_SOURCE}" ]] || die "env example not found: ${ENV_EXAMPLE_SOURCE}" [[ -f "${DATA_COMPOSE_SOURCE}" ]] || die "data compose file not found: ${DATA_COMPOSE_SOURCE}" if [[ -n "${ENV_SOURCE}" ]]; then [[ -f "${ENV_SOURCE}" ]] || die "env file not found: ${ENV_SOURCE}" fi } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --env-file) [[ $# -ge 2 ]] || die "--env-file requires a path" ENV_SOURCE="$2" shift 2 ;; --release-id) [[ $# -ge 2 ]] || die "--release-id requires a value" RELEASE_ID="$2" shift 2 ;; --bin-source) [[ $# -ge 2 ]] || die "--bin-source requires a path" BIN_SOURCE="$2" shift 2 ;; --frontend-source) [[ $# -ge 2 ]] || die "--frontend-source requires a path" FRONTEND_SOURCE="$2" shift 2 ;; --skip-start) SKIP_START="true" shift ;; -h|--help) usage exit 0 ;; *) die "unknown argument: $1" ;; esac done } install_env_file() { install -d -m 0750 "${CONFIG_DIR}" if [[ -n "${ENV_SOURCE}" ]]; then info "installing env file to ${ENV_TARGET}" install -m 0600 "${ENV_SOURCE}" "${ENV_TARGET}" return fi if [[ ! -f "${ENV_TARGET}" ]]; then info "creating env template at ${ENV_TARGET}" install -m 0600 "${ENV_EXAMPLE_SOURCE}" "${ENV_TARGET}" cat >&2 < "${rendered_unit}" install -m 0644 "${rendered_unit}" "${SYSTEMD_UNIT_PATH}" rm -f "${rendered_unit}" systemctl daemon-reload systemctl enable "${SERVICE_NAME}" >/dev/null } restart_service_if_requested() { if [[ "${SKIP_START}" == "true" ]]; then info "skipping service restart" return fi info "restarting ${SERVICE_NAME}" systemctl restart "${SERVICE_NAME}" } print_next_steps() { local gateway_port gateway_port="$(awk -F= '/^[[:space:]]*APP_PORT=/{print $2}' "${ENV_TARGET}" | tail -n1 | tr -d '[:space:]')" gateway_port="${gateway_port:-8084}" cat <