#!/bin/bash # ============================================================================= # ns8-backup-monitor - Installer / Uninstaller # ============================================================================= # Usage: # install.sh - install (interactive) # install.sh --uninstall - remove everything installed by this script # # Requires: root, python3, curl (no git needed) # Tested on: AlmaLinux 8/9, Rocky Linux 8/9 (NS8 supported distros) # # Two systemd units are installed: # ns8-backup-monitor.service - long-running webhook receiver for # Alertmanager failure alerts # ns8-backup-monitor-check.timer - daily one-shot timer that sends a # scheduled recap email regardless of # backup outcome (success or failure) # # ns8-sendmail is NOT in the standard root PATH on NS8 nodes. # The installer verifies it via 'runagent' — the NS8 agent runner — which # is always present when ns8-sendmail is available. # ============================================================================= set -euo pipefail # --- constants ---------------------------------------------------------------- SERVICE_NAME="ns8-backup-monitor" CHECK_SERVICE_NAME="ns8-backup-monitor-check" INSTALL_DIR="/opt/ns8-backup-monitor" CONFIG_DIR="/etc/ns8-backup-monitor" CONFIG_FILE="${CONFIG_DIR}/config.yml" SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" CHECK_SERVICE_FILE="/etc/systemd/system/${CHECK_SERVICE_NAME}.service" CHECK_TIMER_FILE="/etc/systemd/system/${CHECK_SERVICE_NAME}.timer" # Gitea raw base URL for downloading individual files RAW_BASE="https://repo.lelekaos.com/admin/ns8-backup-monitor/raw/branch/main" # Gitea archive URL (no git needed - just curl) ARCHIVE_URL="https://repo.lelekaos.com/admin/ns8-backup-monitor/archive/main.tar.gz" PYTHON=$(command -v python3 || true) # --- colours ------------------------------------------------------------------ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' BLUE='\033[0;34m'; BOLD='\033[1m'; RESET='\033[0m' info() { echo -e "${BLUE}[INFO]${RESET} $*"; } ok() { echo -e "${GREEN}[OK]${RESET} $*"; } warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; } error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; exit 1; } # ============================================================================= # CHECK NS8-SENDMAIL # On NS8 nodes, ns8-sendmail is not in the standard root PATH. # The canonical check is to verify that 'runagent' exists: ns8-sendmail is # always available when the NS8 environment (and runagent) is present. # ============================================================================= check_ns8_sendmail() { # 1. Direct PATH lookup (covers non-standard or manual installs) if command -v ns8-sendmail &>/dev/null; then return 0 fi # 2. runagent presence confirms this is an NS8 node where ns8-sendmail # is available at runtime even if not in root's PATH. if command -v runagent &>/dev/null; then return 0 fi return 1 } # ============================================================================= # UNINSTALL # ============================================================================= do_uninstall() { echo -e "${BOLD}=== ns8-backup-monitor UNINSTALL ===${RESET}" warn "This will stop and remove the service, timer, code and config." read -rp "Continue? [y/N] " confirm [[ "$confirm" =~ ^[Yy]$ ]] || { info "Aborted."; exit 0; } for unit in "$CHECK_SERVICE_NAME" "$SERVICE_NAME"; do if systemctl is-active --quiet "$unit" 2>/dev/null; then info "Stopping ${unit}..." systemctl stop "$unit" fi if systemctl is-enabled --quiet "$unit" 2>/dev/null; then info "Disabling ${unit}..." systemctl disable "$unit" fi done for f in "$CHECK_TIMER_FILE" "$CHECK_SERVICE_FILE" "$SERVICE_FILE"; do if [[ -f "$f" ]]; then info "Removing ${f}..." rm -f "$f" fi done systemctl daemon-reload ok "Systemd units removed." if [[ -d "$INSTALL_DIR" ]]; then info "Removing ${INSTALL_DIR}..." rm -rf "$INSTALL_DIR" ok "Install directory removed." fi if [[ -d "$CONFIG_DIR" ]]; then read -rp "Remove config directory ${CONFIG_DIR}? [y/N] " rmcfg if [[ "$rmcfg" =~ ^[Yy]$ ]]; then rm -rf "$CONFIG_DIR" ok "Config directory removed." else warn "Config kept at ${CONFIG_DIR}." fi fi echo -e "${GREEN}${BOLD}Uninstall complete.${RESET}" } # ============================================================================= # DOWNLOAD SOURCE # Bug fix: declare and assign tmpdir on the same line so that 'set -u' never # sees an unset variable, even if the trap fires before mktemp completes. # ============================================================================= download_source() { local tmpdir; tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' RETURN info "Downloading source archive..." curl -fsSL "$ARCHIVE_URL" -o "${tmpdir}/archive.tar.gz" \ || error "Failed to download archive from ${ARCHIVE_URL}" info "Extracting..." mkdir -p "$INSTALL_DIR" # Gitea archives extract to a subdirectory named -/ tar -xzf "${tmpdir}/archive.tar.gz" -C "${tmpdir}" local extracted_dir extracted_dir=$(find "${tmpdir}" -mindepth 1 -maxdepth 1 -type d | head -n1) [[ -n "$extracted_dir" ]] || error "Could not find extracted directory in archive." cp -a "${extracted_dir}/." "$INSTALL_DIR/" ok "Source ready at ${INSTALL_DIR}." } # ============================================================================= # INSTALL # ============================================================================= do_install() { echo -e "${BOLD}=== ns8-backup-monitor INSTALLER ===${RESET}\n" # --- pre-flight ----------------------------------------------------------- [[ $EUID -eq 0 ]] || error "Run as root (or with sudo)." [[ -n "$PYTHON" ]] || error "python3 not found." command -v curl &>/dev/null || error "curl not found." command -v tar &>/dev/null || error "tar not found." if check_ns8_sendmail; then ok "ns8-sendmail available (NS8 node confirmed)." else warn "Neither ns8-sendmail nor runagent found." warn "Make sure this runs on an NS8 node, or email delivery will fail." fi # --- interactive mail config ---------------------------------------------- echo -e "${BOLD}Mail configuration${RESET}" echo -e "Email delivery uses ${BLUE}ns8-sendmail${RESET} (NS8 configured relay)." echo local default_from="ns8-backup-monitor@$(hostname -f 2>/dev/null || echo localhost)" read -rp "Sender address (From) [${default_from}]: " MAIL_FROM MAIL_FROM="${MAIL_FROM:-$default_from}" local MAIL_TO_LIST=() echo "Recipient addresses (To) - one per line, empty line when done:" while true; do read -rp " Recipient: " addr [[ -z "$addr" ]] && break MAIL_TO_LIST+=("$addr") done [[ ${#MAIL_TO_LIST[@]} -gt 0 ]] || error "At least one recipient is required." read -rp "Subject prefix [[NS8 Backup]]: " SUBJECT_PREFIX SUBJECT_PREFIX="${SUBJECT_PREFIX:-[NS8 Backup]}" # --- scheduled check time ------------------------------------------------- echo echo -e "${BOLD}Scheduled daily recap${RESET}" echo -e "A daily timer will send a backup recap email regardless of outcome." echo -e "Set this to ~30 minutes after your last backup is expected to finish." read -rp "Daily recap time (HH:MM, 24h) [07:00]: " RECAP_TIME RECAP_TIME="${RECAP_TIME:-07:00}" # Validate format [[ "$RECAP_TIME" =~ ^([01][0-9]|2[0-3]):[0-5][0-9]$ ]] \ || error "Invalid time format '${RECAP_TIME}'. Use HH:MM (e.g. 07:00)." RECAP_HOUR="${RECAP_TIME%%:*}" RECAP_MIN="${RECAP_TIME##*:}" echo info "From: $MAIL_FROM" info "To: ${MAIL_TO_LIST[*]}" info "Prefix: $SUBJECT_PREFIX" info "Recap at: ${RECAP_TIME} daily" echo read -rp "Confirm and proceed with install? [Y/n] " go [[ "$go" =~ ^[Nn]$ ]] && { info "Aborted."; exit 0; } echo # --- download source ------------------------------------------------------ if [[ -d "$INSTALL_DIR" ]]; then info "${INSTALL_DIR} already exists - updating source..." rm -rf "$INSTALL_DIR" fi download_source # --- config directory ----------------------------------------------------- mkdir -p "$CONFIG_DIR" chmod 750 "$CONFIG_DIR" if [[ -f "$CONFIG_FILE" ]]; then warn "Config ${CONFIG_FILE} already exists - keeping it." warn "Edit it manually to change mail settings." else info "Writing ${CONFIG_FILE}..." local to_yaml="" for addr in "${MAIL_TO_LIST[@]}"; do to_yaml+=" - \"${addr}\"\n" done cat > "$CONFIG_FILE" << EOF # ns8-backup-monitor configuration # Generated by install.sh on $(date -u '+%Y-%m-%d %H:%M UTC') # Email delivery is handled by ns8-sendmail (NS8 configured relay). mail: from: "${MAIL_FROM}" to: $(printf '%b' "$to_yaml") subject_prefix: "${SUBJECT_PREFIX}" receiver: host: "127.0.0.1" port: 9099 correlator: wait_seconds: 30 recent_window: 3600 redis: socket: "/var/lib/nethserver/cluster/state/redis.sock" repo_check: timeout: 60 restic_flags: "" logging: level: INFO file: "/var/log/ns8-backup-monitor.log" EOF chmod 640 "$CONFIG_FILE" ok "Config written." fi # --- systemd units -------------------------------------------------------- info "Installing systemd units..." # Webhook receiver (long-running) cp "${INSTALL_DIR}/deploy/ns8-backup-monitor.service" "$SERVICE_FILE" # Scheduled check service (one-shot, invoked by timer) cp "${INSTALL_DIR}/deploy/ns8-backup-monitor-check.service" "$CHECK_SERVICE_FILE" # Timer: inject the configured recap time into the unit file sed "s/OnCalendar=\*-\*-\* 07:00:00/OnCalendar=*-*-* ${RECAP_HOUR}:${RECAP_MIN}:00/" \ "${INSTALL_DIR}/deploy/ns8-backup-monitor-check.timer" > "$CHECK_TIMER_FILE" systemctl daemon-reload systemctl enable --now "$SERVICE_NAME" systemctl enable --now "${CHECK_SERVICE_NAME}.timer" ok "Webhook service and daily recap timer enabled and started." # --- done ----------------------------------------------------------------- echo echo -e "${GREEN}${BOLD}Installation complete.${RESET}" echo -e " Config: ${CONFIG_FILE}" echo -e " Webhook: systemctl status ${SERVICE_NAME}" echo -e " Daily recap: systemctl status ${CHECK_SERVICE_NAME}.timer" echo -e " Logs: journalctl -u ${SERVICE_NAME} -f" echo -e " Manual test: systemctl start ${CHECK_SERVICE_NAME}" echo echo -e "To uninstall: ${BOLD}bash ${INSTALL_DIR}/deploy/install.sh --uninstall${RESET}" echo -e "To update: ${BOLD}bash <(curl -fsSL ${RAW_BASE}/deploy/install.sh)${RESET}" } # ============================================================================= # ENTRYPOINT # ============================================================================= case "${1:-}" in --uninstall|-u|uninstall) do_uninstall ;; --help|-h) echo "Usage: $0 [--uninstall]" exit 0 ;; "") do_install ;; *) error "Unknown argument: $1. Use --uninstall to remove." ;; esac