Files
R-clone-setup/monitor/collector/deploy_agent.sh
2026-08-18 11:31:53 +05:30

172 lines
7.2 KiB
Bash

#!/usr/bin/env bash
# =============================================================================
# rclone-monitor agent installer — served by the collector with the live
# AGENT_TOKEN and COLLECTOR_URL injected at download time.
#
# curl -fsSL -u admin "https://rclone.seekright.com/deploy_agent.sh" -o /tmp/deploy_agent.sh
# sudo bash /tmp/deploy_agent.sh <MACHINE_ID> [SITE]
# rm /tmp/deploy_agent.sh # the file contains the live token
#
# What it does (read-only towards the existing stack):
# 1. finds the running rclone-synology-sync container
# 2. auto-discovers its /logs and upload-source host paths via docker inspect
# 3. writes /opt/rclone-agent/{agent.py,Dockerfile,docker-compose.yml,.env}
# 4. builds + starts the monitoring agent container
# Re-running with no args upgrades in place, keeping the machine's identity.
# =============================================================================
set -euo pipefail
AGENT_TOKEN="__AGENT_TOKEN__"
COLLECTOR_URL="__COLLECTOR_URL__"
INSTALL_DIR="${INSTALL_DIR:-/opt/rclone-agent}"
SYNC_CONTAINER="${SYNC_CONTAINER:-rclone-synology-sync}"
RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'; NC='\033[0m'
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
die() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
# Sentinel is split so server-side injection can never rewrite it (RMM lesson)
[ "$AGENT_TOKEN" = "__AGENT_""TOKEN__" ] && \
die "This copy has no token. Download it FROM THE SERVER, don't copy it out of the repo."
[ "$(id -u)" -eq 0 ] || die "Run with sudo (docker access needed)."
command -v docker >/dev/null || die "docker not found on this machine."
command -v curl >/dev/null || die "curl not found on this machine."
if docker compose version >/dev/null 2>&1; then COMPOSE="docker compose";
elif command -v docker-compose >/dev/null; then COMPOSE="docker-compose";
else die "Neither 'docker compose' nor 'docker-compose' found."; fi
# ---- identity ---------------------------------------------------------------
MACHINE_ID="${1:-}"
SITE="${2:-}"
if [ -z "$MACHINE_ID" ] && [ -f "$INSTALL_DIR/.env" ]; then
# upgrade mode: keep the existing identity
MACHINE_ID=$(grep '^MACHINE_ID=' "$INSTALL_DIR/.env" | cut -d= -f2-)
SITE=${SITE:-$(grep '^SITE=' "$INSTALL_DIR/.env" | cut -d= -f2-)}
info "Upgrade mode — keeping identity ${MACHINE_ID} (${SITE})"
fi
[ -n "$MACHINE_ID" ] || die "Usage: sudo bash deploy_agent.sh <MACHINE_ID> [SITE]
MACHINE_ID is required on a fresh install (it names this machine on the dashboard)."
echo "$MACHINE_ID" | grep -qE '^[A-Za-z0-9._-]+$' || die "MACHINE_ID may only contain letters, digits, . _ -"
SITE="${SITE:-$MACHINE_ID}"
# ---- prerequisites on this machine ------------------------------------------
docker inspect "$SYNC_CONTAINER" >/dev/null 2>&1 || \
die "Container '$SYNC_CONTAINER' not found. This machine doesn't seem to run the rclone sync (set SYNC_CONTAINER=<name> if it's named differently)."
info "Checking collector reachability: $COLLECTOR_URL"
curl -fsS -m 10 "$COLLECTOR_URL/api/health" >/dev/null || \
die "Cannot reach $COLLECTOR_URL/api/health from this machine — fix network/DNS first."
ok "Collector reachable"
# ---- auto-discover the sync container's host paths --------------------------
mount_src() {
docker inspect "$SYNC_CONTAINER" \
--format '{{range .Mounts}}{{if eq .Destination "'"$1"'"}}{{.Source}}{{end}}{{end}}'
}
LOGS_DIR=$(mount_src /logs)
IMAGE_DIR=$(mount_src /sources/image_root_dir)
CSV_DIR=$(mount_src /sources/csv_files)
[ -n "$LOGS_DIR" ] || die "The sync container has no /logs mount — nothing to monitor."
ok "logs: $LOGS_DIR"
WATCH=""
VOLS=""
if [ -n "$IMAGE_DIR" ]; then
ok "images: $IMAGE_DIR"
WATCH="/sources/image_root_dir"
VOLS=" - \${IMAGE_DIR}:/sources/image_root_dir:ro"$'\n'
else
info "no /sources/image_root_dir mount — image pending-detection disabled"
fi
if [ -n "$CSV_DIR" ]; then
ok "csv: $CSV_DIR"
WATCH="${WATCH:+$WATCH,}/sources/csv_files"
VOLS="${VOLS} - \${CSV_DIR}:/sources/csv_files:ro"$'\n'
fi
# ---- fetch agent source from the collector (single source of truth) ---------
mkdir -p "$INSTALL_DIR"
info "Downloading agent.py from the collector"
curl -fsS -m 30 -H "Authorization: Bearer $AGENT_TOKEN" \
"$COLLECTOR_URL/agent.py" -o "$INSTALL_DIR/agent.py" || die "agent.py download failed"
head -1 "$INSTALL_DIR/agent.py" | grep -q python || die "agent.py download looks wrong (auth?)"
# ---- write install files ----------------------------------------------------
cat > "$INSTALL_DIR/Dockerfile" <<'EOF'
FROM python:3.12-slim
WORKDIR /app
COPY agent.py .
CMD ["python", "-u", "agent.py"]
EOF
cat > "$INSTALL_DIR/.env" <<EOF
# written by deploy_agent.sh $(date '+%Y-%m-%d %H:%M') — edit + re-run compose to change
MACHINE_ID=$MACHINE_ID
SITE=$SITE
COLLECTOR_URL=$COLLECTOR_URL
AGENT_TOKEN=$AGENT_TOKEN
SYNC_LOGS_DIR=$LOGS_DIR
IMAGE_DIR=${IMAGE_DIR:-/dev/null}
CSV_DIR=${CSV_DIR:-/dev/null}
WATCH_DIRS=$WATCH
# max files listed per watched dir (raise on machines with huge image trees)
MAX_INV_FILES=20000
# rebuild/ship the file inventory at most this often (seconds)
INV_INTERVAL_S=60
EOF
chmod 600 "$INSTALL_DIR/.env"
cat > "$INSTALL_DIR/docker-compose.yml" <<EOF
# Auto-generated by deploy_agent.sh — monitoring agent only.
# Touches NOTHING in the existing stack; all mounts read-only.
services:
rclone-agent:
build: .
container_name: rclone-agent
environment:
- MACHINE_ID=\${MACHINE_ID}
- SITE=\${SITE}
- COLLECTOR_URL=\${COLLECTOR_URL}
- AGENT_TOKEN=\${AGENT_TOKEN}
- WATCH_DIRS=\${WATCH_DIRS}
- MAX_INV_FILES=\${MAX_INV_FILES}
- INV_INTERVAL_S=\${INV_INTERVAL_S}
volumes:
- \${SYNC_LOGS_DIR}:/logs:ro
$VOLS restart: unless-stopped
logging:
options: { max-size: "10m", max-file: "2" }
EOF
# ---- base image: pull, or fetch from the collector if the registry is blocked
# (office machines often can't reach Docker Hub — TLS-intercepting firewalls)
ensure_base_image() {
docker image inspect python:3.12-slim >/dev/null 2>&1 && return 0
info "Pulling python:3.12-slim from Docker Hub"
docker pull python:3.12-slim >/dev/null 2>&1 && return 0
info "Pull failed (registry unreachable/blocked) — fetching base image from the collector instead"
curl -fsS -m 300 -H "Authorization: Bearer $AGENT_TOKEN" \
"$COLLECTOR_URL/base-image" -o /tmp/python-base.tar.gz || \
die "Could not fetch base image from the collector either. Admin: stage it on the server (see /base-image endpoint message)."
docker load -i /tmp/python-base.tar.gz >/dev/null || die "docker load of fetched base image failed"
rm -f /tmp/python-base.tar.gz
ok "Base image loaded from collector"
}
ensure_base_image
# ---- replace any previous agent and start -----------------------------------
docker rm -f rclone-agent >/dev/null 2>&1 && info "Removed previous rclone-agent container" || true
info "Building + starting the agent"
(cd "$INSTALL_DIR" && $COMPOSE up -d --build)
sleep 4
echo "---- agent log ----"
docker logs --tail 5 rclone-agent || true
echo "-------------------"
ok "Installed. '$MACHINE_ID' appears on the dashboard within ~10s:"
echo -e " ${CYAN}${COLLECTOR_URL}/${NC}"
echo " (sync badge shows 'no rounds yet' until the next sync round completes)"