172 lines
7.2 KiB
Bash
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)"
|