#!/usr/bin/env bash
# Auto-installer for the Sentinel agent on a GPU host.
#
# Detects the host's NVIDIA device nodes, generates a systemd unit with
# literal DeviceAllow= entries for each of them (globs don't cross "/",
# see AG-16/AG-17), installs user/dirs/binary/helper/sudoers/unit, and
# (re)starts the service.
#
# Update commands (protocol v2): also installs the root-owned
# sentinel-update-helper plus the exact-argv sudoers drop-in, and generates a
# unit WITHOUT NoNewPrivileges/ProtectSystem=strict — sudo's setuid transition
# and apt's writes to /usr cannot cross those. Compensating controls: the
# helper's fixed case arms and the 4-line argv allowlist (see
# docs/agent/AG-23-update-commands.md + project/v2/protocol.md §7).
#
# Usage:
#   sudo bash install-agent.sh [path-to-sentinel-agent-binary]
#
# The binary is looked up, in order: the argument, ./dist/sentinel-agent,
# ../dist/sentinel-agent (relative to this script). If a config is not yet
# present the service is installed but left disabled; run
#   sudo -u sentinel sentinel-agent init
# then `sudo systemctl enable --now sentinel-agent`.
set -euo pipefail

if [[ $EUID -ne 0 ]]; then
  echo "ERROR: run as root (sudo bash $0)" >&2
  exit 1
fi

SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
UNIT_DST=/etc/systemd/system/sentinel-agent.service
BIN_DST=/usr/local/bin/sentinel-agent
HELPER_SRC="$SCRIPT_DIR/sentinel-update-helper"
HELPER_DST=/usr/local/sbin/sentinel-update-helper
SUDOERS_DST=/etc/sudoers.d/sentinel-agent

# --- locate the binary -----------------------------------------------------
BIN="${1:-}"
if [[ -z $BIN ]]; then
  for cand in "$SCRIPT_DIR/dist/sentinel-agent" "$SCRIPT_DIR/../dist/sentinel-agent" "$(pwd)/sentinel-agent"; do
    [[ -f $cand ]] && BIN=$cand && break
  done
fi
if [[ -z $BIN || ! -f $BIN ]]; then
  echo "ERROR: sentinel-agent binary not found." >&2
  echo "Pass it as an argument: sudo bash $0 /path/to/sentinel-agent" >&2
  exit 1
fi
BIN=$(readlink -f "$BIN")
echo "==> Binary: $BIN"

# --- update-command helper + sudoers (before GPU detection: must land even
#     on a CPU-only host whose install exits early at the "no NVIDIA nodes"
#     check) -------------------------------------------------------------------
# The copy-install flow curls installer + helper + binary into one directory;
# accept the helper next to this script, next to the binary, or in the cwd.
HELPER="$HELPER_SRC"
if [[ ! -f $HELPER ]]; then
  for cand in "$(dirname "$BIN")/sentinel-update-helper" "$(pwd)/sentinel-update-helper"; do
    [[ -f $cand ]] && HELPER=$cand && break
  done
fi
if [[ -f $HELPER ]]; then
  install -d -o root -g root -m 0755 /usr/local/sbin
  install -o root -g root -m 0755 "$HELPER" "$HELPER_DST"
  echo "==> Installed $HELPER_DST (root:root 0755)"
else
  echo "WARNING: sentinel-update-helper not found — remote update commands will report 'unsupported'." >&2
fi

if [[ -f $HELPER_DST ]] && command -v visudo >/dev/null 2>&1; then
  # Four literal argv lines — no wildcards, no env passthrough. Validated with
  # visudo -cf BEFORE it goes live so a bad edit can never break sudo.
  sudoers_tmp=$(mktemp)
  {
    echo "# Managed by sentinel-agent install-agent.sh — do not edit by hand."
    echo "# Exact argv only (project/v2/protocol.md §7); sudo -n never prompts."
    for sub in check update upgrade reboot; do
      echo "sentinel ALL=(root) NOPASSWD: $HELPER_DST $sub"
    done
  } > "$sudoers_tmp"
  chown root:root "$sudoers_tmp"
  chmod 0440 "$sudoers_tmp"
  if visudo -cf "$sudoers_tmp" >/dev/null; then
    install -o root -g root -m 0440 "$sudoers_tmp" "$SUDOERS_DST"
    echo "==> Installed $SUDOERS_DST (4 exact-argv NOPASSWD lines, visudo-validated)"
  else
    echo "ERROR: generated sudoers file failed visudo -cf; NOT installed." >&2
    visudo -cf "$sudoers_tmp" || true
    rm -f "$sudoers_tmp"
    exit 1
  fi
  rm -f "$sudoers_tmp"
elif [[ ! -f $HELPER_DST ]]; then
  echo "WARNING: helper absent — skipping sudoers drop-in." >&2
else
  echo "WARNING: visudo not found — skipping sudoers drop-in (update commands disabled)." >&2
fi

# --- detect NVIDIA device nodes --------------------------------------------
gpu_nodes=()
for node in /dev/nvidiactl /dev/nvidia-modeset /dev/nvidia-uvm /dev/nvidia-uvm-tools; do
  [[ -e $node ]] && gpu_nodes+=("$node")
done
# GPU cards, sorted numerically so nvidia2 follows nvidia1 (not nvidia10-style sort)
shopt -s nullglob
cards=(/dev/nvidia[0-9]*)
shopt -u nullglob
if ((${#cards[@]} == 0)); then
  echo "WARNING: no /dev/nvidia[0-9]* nodes found — is the NVIDIA driver loaded?" >&2
else
  mapfile -t cards < <(printf '%s\n' "${cards[@]}" | sed 's|/dev/nvidia||' | sort -n | sed 's|^|/dev/nvidia|')
  gpu_nodes+=("${cards[@]}")
fi
caps=(/dev/nvidia-caps/nvidia-cap*)
gpu_nodes+=("${caps[@]}")

if ((${#gpu_nodes[@]} == 0)); then
  echo "ERROR: no NVIDIA device nodes detected; nothing to allow." >&2
  exit 1
fi

echo "==> Detected ${#cards[@]} GPU(s) and ${#gpu_nodes[@]} device nodes:"
printf '    %s\n' "${gpu_nodes[@]}"

# --- user + directories ----------------------------------------------------
if ! id sentinel &>/dev/null; then
  useradd --system --no-create-home --shell /usr/sbin/nologin sentinel
  echo "==> Created system user 'sentinel'"
fi
install -d -o sentinel -g sentinel -m 0750 /etc/sentinel
install -d -o sentinel -g sentinel -m 0700 /var/lib/sentinel

# --- install binary --------------------------------------------------------
install -m 0755 "$BIN" "$BIN_DST"
echo "==> Installed $BIN_DST"

# --- generate the unit with literal DeviceAllow entries --------------------
{
  cat <<'EOF'
[Unit]
Description=Sentinel agent — AI/GPU server monitoring collector
Documentation=file:///usr/share/doc/sentinel-agent/README.md
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=sentinel
Group=sentinel
ExecStart=/usr/local/bin/sentinel-agent run --config /etc/sentinel/agent.yaml
Restart=on-failure
RestartSec=5s
# Unbuffered stdout so journald sees lines immediately.
Environment=PYTHONUNBUFFERED=1

# --- hardening ---
# v2 update commands deliberately DROP NoNewPrivileges (it blocks sudo's
# setuid transition) and ProtectSystem=strict (its read-only /usr mount
# namespace is inherited by sudo'd children, so apt could never install).
# Compensating controls: the root-owned sentinel-update-helper's fixed case
# arms + the 4 exact-argv sudoers lines (project/v2/protocol.md §7), admin-
# gated dispatch and full audit on the server side.
ProtectHome=true
PrivateTmp=true
# NVML needs the GPU character devices; keep device access on.
PrivateDevices=false
# DeviceAllow globs do not cross "/" (and on some systemd versions do not match
# at all), so this unit lists the host's literal nodes. This file was GENERATED
# by packaging/install-agent.sh on this host — re-run it after adding GPUs.
EOF
  for node in "${gpu_nodes[@]}"; do
    printf 'DeviceAllow=%s\n' "$node"
  done
  cat <<'EOF'
# The buffer lives under /var/lib/sentinel. ReadWritePaths stays listed so the
# unit keeps working unchanged if ProtectSystem is ever re-added.
ReadWritePaths=/var/lib/sentinel
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF
} > "$UNIT_DST"
echo "==> Generated $UNIT_DST"

# --- reload + (conditionally) restart ---------------------------------------
# Capture liveness BEFORE daemon-reload: `enable --now` alone never applies a
# regenerated unit to an already-running service, so an in-place upgrade would
# silently keep the old (over-hardened) unit until the next reboot.
was_active=0
systemctl is-active --quiet sentinel-agent 2>/dev/null && was_active=1 || true

systemctl daemon-reload

if [[ -f /etc/sentinel/agent.yaml ]]; then
  if (( was_active )); then
    systemctl restart sentinel-agent
    echo "==> Service restarted (unit regenerated in place)."
  else
    systemctl enable --now sentinel-agent
    echo "==> Service enabled and started."
  fi
  sleep 2
  if systemctl is-active --quiet sentinel-agent; then
    echo "==> sentinel-agent is ACTIVE. Recent logs:"
    journalctl -u sentinel-agent -n 15 --no-pager || true
  else
    echo "!! Service failed to start. Logs:" >&2
    journalctl -u sentinel-agent -n 30 --no-pager >&2 || true
    exit 1
  fi
else
  echo "==> No /etc/sentinel/agent.yaml found — service installed but NOT started."
  echo "    Configure it, then:  sudo systemctl enable --now sentinel-agent"
  echo "      sudo -u sentinel sentinel-agent init"
fi
