596 lines
26 KiB
Bash
596 lines
26 KiB
Bash
|
|
#!/bin/bash
|
||
|
|
# =============================================================================
|
||
|
|
# NAS08 System Backup Script
|
||
|
|
# =============================================================================
|
||
|
|
# Backs up OMV config, Docker Compose files, app data, and system /etc
|
||
|
|
# Writes a single dated .tar.gz archive to the NAS data drive
|
||
|
|
# Retains the last 10 backups (~30 days), auto-removes older ones
|
||
|
|
# Sends an HTML email report on completion (via OMV Postfix / Gmail relay).
|
||
|
|
#
|
||
|
|
# Schedule via OMV Scheduled Jobs (cron):
|
||
|
|
# 0 2 */3 * * (2:00 AM every 3 days)
|
||
|
|
#
|
||
|
|
# Backup destination: /export/kingdezigns-all/Backups/NAS08/
|
||
|
|
# Script location: /usr/scripts/omv/nas08-backup.sh
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
set -uo pipefail
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# CONFIGURATION
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
HOSTNAME_LABEL=$(hostname -s 2>/dev/null || echo "NAS08")
|
||
|
|
REPORT_TO="rufus.king@kingdezigns.com"
|
||
|
|
REPORT_FROM="backup-monitor@nas08.local" # Display only — Postfix uses your Gmail relay
|
||
|
|
|
||
|
|
BACKUP_ROOT="/export/kingdezigns-all/Backups/NAS08"
|
||
|
|
TMP_BASE="/export/kingdezigns-all/Backups/.tmp"
|
||
|
|
DOCKER_DIR="/export/kingdezigns-all/Docker"
|
||
|
|
DATE=$(date +%Y-%m-%d)
|
||
|
|
TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
|
||
|
|
REPORT_TIME=$(date "+%Y-%m-%d %H:%M:%S")
|
||
|
|
ARCHIVE_NAME="nas08-backup-${DATE}.tar.gz"
|
||
|
|
TMP_DIR="${TMP_BASE}/nas08-backup-${TIMESTAMP}"
|
||
|
|
RETAIN_COUNT=10
|
||
|
|
LOG_FILE="${BACKUP_ROOT}/backup.log"
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# STATUS TRACKING
|
||
|
|
# OVERALL_STATUS: SUCCESS | WARNING | FAILED
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
OVERALL_STATUS="SUCCESS"
|
||
|
|
SECTION_ROWS_HTML=""
|
||
|
|
WARNING_COUNT=0
|
||
|
|
ERROR_COUNT=0
|
||
|
|
|
||
|
|
# Append a result row to the section table
|
||
|
|
# Usage: add_section_row "Section name" "STATUS" "Detail message"
|
||
|
|
# STATUS: OK | WARNING | ERROR | SKIPPED
|
||
|
|
add_section_row() {
|
||
|
|
local section="$1" status="$2" detail="$3"
|
||
|
|
|
||
|
|
case "$status" in
|
||
|
|
OK)
|
||
|
|
BADGE="<span style=\"background:#d1fae5;color:#065f46;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:700;\">✓ OK</span>"
|
||
|
|
;;
|
||
|
|
WARNING)
|
||
|
|
BADGE="<span style=\"background:#fef3c7;color:#92400e;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:700;\">⚠ WARNING</span>"
|
||
|
|
WARNING_COUNT=$((WARNING_COUNT + 1))
|
||
|
|
[[ "$OVERALL_STATUS" == "SUCCESS" ]] && OVERALL_STATUS="WARNING"
|
||
|
|
;;
|
||
|
|
ERROR)
|
||
|
|
BADGE="<span style=\"background:#fee2e2;color:#991b1b;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:700;\">✗ ERROR</span>"
|
||
|
|
ERROR_COUNT=$((ERROR_COUNT + 1))
|
||
|
|
OVERALL_STATUS="FAILED"
|
||
|
|
;;
|
||
|
|
SKIPPED)
|
||
|
|
BADGE="<span style=\"background:#f3f4f6;color:#6b7280;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:700;\">— SKIPPED</span>"
|
||
|
|
;;
|
||
|
|
esac
|
||
|
|
|
||
|
|
SECTION_ROWS_HTML+="
|
||
|
|
<tr>
|
||
|
|
<td style=\"padding:9px 14px;border-bottom:1px solid #f3f4f6;font-size:13px;color:#111827;font-weight:600;\">$section</td>
|
||
|
|
<td style=\"padding:9px 14px;border-bottom:1px solid #f3f4f6;\">$BADGE</td>
|
||
|
|
<td style=\"padding:9px 14px;border-bottom:1px solid #f3f4f6;font-size:12px;color:#6b7280;\">$detail</td>
|
||
|
|
</tr>"
|
||
|
|
}
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# LOGGING
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
log() {
|
||
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||
|
|
}
|
||
|
|
|
||
|
|
log_section() {
|
||
|
|
echo "" | tee -a "$LOG_FILE"
|
||
|
|
echo "------------------------------------------------------------" | tee -a "$LOG_FILE"
|
||
|
|
log "$1"
|
||
|
|
echo "------------------------------------------------------------" | tee -a "$LOG_FILE"
|
||
|
|
}
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# PRE-FLIGHT CHECKS
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
log_section "NAS08 Backup Starting"
|
||
|
|
|
||
|
|
if [ ! -d "/export/kingdezigns-all" ]; then
|
||
|
|
log "ERROR: /export/kingdezigns-all is not mounted or accessible. Aborting."
|
||
|
|
add_section_row "Pre-flight check" "ERROR" "/export/kingdezigns-all not mounted or accessible"
|
||
|
|
OVERALL_STATUS="FAILED"
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
mkdir -p "$BACKUP_ROOT"
|
||
|
|
mkdir -p "$TMP_DIR"
|
||
|
|
log "Temporary working directory: $TMP_DIR"
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# SECTION 1: OMV CONFIGURATION
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
log_section "Backing up OMV configuration"
|
||
|
|
|
||
|
|
OMV_DIR="${TMP_DIR}/omv-config"
|
||
|
|
mkdir -p "$OMV_DIR"
|
||
|
|
OMV_STATUS="OK"
|
||
|
|
OMV_DETAIL=""
|
||
|
|
|
||
|
|
# omv-confbak is optional — raw config.xml is the primary backup and fully sufficient.
|
||
|
|
if command -v omv-confbak &>/dev/null; then
|
||
|
|
if omv-confbak "${OMV_DIR}/omv-config.xml" 2>/dev/null; then
|
||
|
|
log "omv-confbak completed successfully"
|
||
|
|
OMV_DETAIL="omv-confbak exported + raw config.xml copied"
|
||
|
|
else
|
||
|
|
log "WARNING: omv-confbak failed or returned non-zero — raw config.xml still captured"
|
||
|
|
OMV_DETAIL="omv-confbak failed (non-fatal) + raw config.xml copied"
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
log "omv-confbak not present — using raw config.xml (this is normal)"
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [ -f /etc/openmediavault/config.xml ]; then
|
||
|
|
cp /etc/openmediavault/config.xml "${OMV_DIR}/config.xml.raw"
|
||
|
|
log "Raw OMV config.xml copied"
|
||
|
|
[[ -z "$OMV_DETAIL" ]] && OMV_DETAIL="raw config.xml copied"
|
||
|
|
else
|
||
|
|
log "WARNING: /etc/openmediavault/config.xml not found — OMV config not backed up"
|
||
|
|
OMV_STATUS="WARNING"
|
||
|
|
OMV_DETAIL="config.xml not found at /etc/openmediavault/config.xml"
|
||
|
|
fi
|
||
|
|
|
||
|
|
add_section_row "OMV Configuration" "$OMV_STATUS" "$OMV_DETAIL"
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# SECTION 2: SYSTEM /etc
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
log_section "Backing up /etc (system configuration)"
|
||
|
|
|
||
|
|
ETC_DIR="${TMP_DIR}/etc"
|
||
|
|
mkdir -p "$ETC_DIR"
|
||
|
|
|
||
|
|
if rsync -a --quiet \
|
||
|
|
--exclude='/etc/mtab' \
|
||
|
|
--exclude='/etc/fstab.d' \
|
||
|
|
--exclude='/etc/blkid.tab' \
|
||
|
|
/etc/ "${ETC_DIR}/" 2>/dev/null; then
|
||
|
|
log "/etc backed up successfully"
|
||
|
|
add_section_row "System /etc" "OK" "rsync completed successfully"
|
||
|
|
else
|
||
|
|
log "WARNING: /etc rsync encountered errors"
|
||
|
|
add_section_row "System /etc" "WARNING" "rsync completed with errors"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# SECTION 3: DOCKER COMPOSE FILES & GLOBAL ENV
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
log_section "Backing up Docker Compose files"
|
||
|
|
|
||
|
|
COMPOSE_DIR="${TMP_DIR}/docker-compose"
|
||
|
|
mkdir -p "$COMPOSE_DIR"
|
||
|
|
|
||
|
|
if [ -d "${DOCKER_DIR}/Compose" ]; then
|
||
|
|
if rsync -a --quiet \
|
||
|
|
--exclude='vaultwarden/icon_cache/' \
|
||
|
|
"${DOCKER_DIR}/Compose/" "${COMPOSE_DIR}/" 2>/dev/null; then
|
||
|
|
log "Docker Compose directory backed up (Vaultwarden icon_cache excluded)"
|
||
|
|
# Count compose files for the detail line
|
||
|
|
COMPOSE_COUNT=$(find "${COMPOSE_DIR}" -name "docker-compose.yml" -o -name "compose.yml" 2>/dev/null | wc -l)
|
||
|
|
add_section_row "Docker Compose Files" "OK" "${COMPOSE_COUNT} compose file(s) · vaultwarden/icon_cache excluded"
|
||
|
|
else
|
||
|
|
log "WARNING: Docker Compose rsync encountered errors"
|
||
|
|
add_section_row "Docker Compose Files" "WARNING" "rsync completed with errors"
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
log "WARNING: ${DOCKER_DIR}/Compose not found — skipping"
|
||
|
|
add_section_row "Docker Compose Files" "WARNING" "${DOCKER_DIR}/Compose not found"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# SECTION 4: APP DATA (Pi-hole, Plex, Shared)
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
log_section "Backing up application data"
|
||
|
|
|
||
|
|
APPDATA_DIR="${TMP_DIR}/app-data"
|
||
|
|
mkdir -p "$APPDATA_DIR"
|
||
|
|
APPDATA_WARN=0
|
||
|
|
APPDATA_DETAIL=""
|
||
|
|
|
||
|
|
# Pi-hole
|
||
|
|
if [ -d "${DOCKER_DIR}/pihole" ]; then
|
||
|
|
if rsync -a --quiet "${DOCKER_DIR}/pihole/" "${APPDATA_DIR}/pihole/" 2>/dev/null; then
|
||
|
|
log "Pi-hole data backed up"
|
||
|
|
APPDATA_DETAIL="${APPDATA_DETAIL} Pi-hole ✓"
|
||
|
|
else
|
||
|
|
log "WARNING: Pi-hole rsync encountered errors"
|
||
|
|
APPDATA_DETAIL="${APPDATA_DETAIL} Pi-hole ⚠"
|
||
|
|
APPDATA_WARN=$((APPDATA_WARN + 1))
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
log "WARNING: ${DOCKER_DIR}/pihole not found — skipping"
|
||
|
|
APPDATA_DETAIL="${APPDATA_DETAIL} Pi-hole —"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Plex (Cache, Logs, Crash Reports excluded)
|
||
|
|
if [ -d "${DOCKER_DIR}/plex" ]; then
|
||
|
|
if rsync -a --quiet \
|
||
|
|
--exclude='Plex Media Server/Cache/' \
|
||
|
|
--exclude='Plex Media Server/Logs/' \
|
||
|
|
--exclude='Plex Media Server/Crash Reports/' \
|
||
|
|
"${DOCKER_DIR}/plex/" "${APPDATA_DIR}/plex/" 2>/dev/null; then
|
||
|
|
log "Plex data backed up (Cache, Logs, Crash Reports excluded)"
|
||
|
|
APPDATA_DETAIL="${APPDATA_DETAIL} · Plex ✓"
|
||
|
|
else
|
||
|
|
log "WARNING: Plex rsync encountered errors"
|
||
|
|
APPDATA_DETAIL="${APPDATA_DETAIL} · Plex ⚠"
|
||
|
|
APPDATA_WARN=$((APPDATA_WARN + 1))
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
log "WARNING: ${DOCKER_DIR}/plex not found — skipping"
|
||
|
|
APPDATA_DETAIL="${APPDATA_DETAIL} · Plex —"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Shared folder
|
||
|
|
if [ -d "${DOCKER_DIR}/Shared" ]; then
|
||
|
|
if rsync -a --quiet "${DOCKER_DIR}/Shared/" "${APPDATA_DIR}/shared/" 2>/dev/null; then
|
||
|
|
log "Shared folder backed up"
|
||
|
|
APPDATA_DETAIL="${APPDATA_DETAIL} · Shared ✓"
|
||
|
|
else
|
||
|
|
log "WARNING: Shared rsync encountered errors"
|
||
|
|
APPDATA_DETAIL="${APPDATA_DETAIL} · Shared ⚠"
|
||
|
|
APPDATA_WARN=$((APPDATA_WARN + 1))
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
log "WARNING: ${DOCKER_DIR}/Shared not found — skipping"
|
||
|
|
APPDATA_DETAIL="${APPDATA_DETAIL} · Shared —"
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [ "$APPDATA_WARN" -eq 0 ]; then
|
||
|
|
add_section_row "App Data (Pi-hole / Plex / Shared)" "OK" "${APPDATA_DETAIL# }"
|
||
|
|
else
|
||
|
|
add_section_row "App Data (Pi-hole / Plex / Shared)" "WARNING" "$APPDATA_WARN item(s) missing or failed ·${APPDATA_DETAIL}"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# SECTION 5: CRONTAB & SCHEDULED JOBS
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
log_section "Backing up crontab and scheduled jobs"
|
||
|
|
|
||
|
|
CRON_DIR="${TMP_DIR}/cron"
|
||
|
|
mkdir -p "$CRON_DIR"
|
||
|
|
CRON_WARN=0
|
||
|
|
|
||
|
|
crontab -l > "${CRON_DIR}/root-crontab.txt" 2>/dev/null \
|
||
|
|
&& log "Root crontab saved" \
|
||
|
|
|| { log "No root crontab found"; CRON_WARN=$((CRON_WARN + 1)); }
|
||
|
|
|
||
|
|
if [ -d /var/spool/cron/crontabs ]; then
|
||
|
|
cp -r /var/spool/cron/crontabs/ "${CRON_DIR}/crontabs/" \
|
||
|
|
&& log "All crontabs copied" \
|
||
|
|
|| { log "WARNING: crontabs copy failed"; CRON_WARN=$((CRON_WARN + 1)); }
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [ -d /etc/cron.d ]; then
|
||
|
|
cp -r /etc/cron.d/ "${CRON_DIR}/cron.d/" \
|
||
|
|
&& log "cron.d copied" \
|
||
|
|
|| { log "WARNING: cron.d copy failed"; CRON_WARN=$((CRON_WARN + 1)); }
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [ "$CRON_WARN" -eq 0 ]; then
|
||
|
|
add_section_row "Crontab / Scheduled Jobs" "OK" "root crontab, crontabs/, cron.d/ saved"
|
||
|
|
else
|
||
|
|
add_section_row "Crontab / Scheduled Jobs" "WARNING" "$CRON_WARN item(s) missing or failed to copy"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# SECTION 6: BACKUP MANIFEST
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
log_section "Generating backup manifest"
|
||
|
|
|
||
|
|
OMV_VER=$(dpkg -l openmediavault 2>/dev/null | awk '/^ii/{print $3}' || echo "Unknown")
|
||
|
|
KERNEL_VER=$(uname -r)
|
||
|
|
HOST_IP=$(hostname -I | awk '{print $1}')
|
||
|
|
DOCKER_VER=$(docker --version 2>/dev/null | awk '{print $3}' | tr -d ',' || echo "Unknown")
|
||
|
|
|
||
|
|
MANIFEST="${TMP_DIR}/MANIFEST.txt"
|
||
|
|
cat > "$MANIFEST" <<MANIFEST_EOF
|
||
|
|
=============================================================
|
||
|
|
NAS08 BACKUP MANIFEST
|
||
|
|
=============================================================
|
||
|
|
Hostname: $(hostname)
|
||
|
|
Date: $(date)
|
||
|
|
Kernel: ${KERNEL_VER}
|
||
|
|
OMV Version: ${OMV_VER}
|
||
|
|
Docker: ${DOCKER_VER}
|
||
|
|
IP Address: ${HOST_IP}
|
||
|
|
|
||
|
|
CONTENTS:
|
||
|
|
omv-config/ - OMV exported config + raw config.xml
|
||
|
|
etc/ - Full /etc system configuration
|
||
|
|
docker-compose/ - All Docker Compose files and global.env
|
||
|
|
app-data/ - Pi-hole, Plex, Shared app data
|
||
|
|
cron/ - Root crontab and scheduled jobs
|
||
|
|
MANIFEST.txt - This file
|
||
|
|
|
||
|
|
RESTORE NOTES:
|
||
|
|
1. Flash fresh OMV to SD card
|
||
|
|
2. SSH in and restore /etc from etc/ folder
|
||
|
|
3. Import OMV config via omv-confbak or config.xml.raw
|
||
|
|
4. Restore Docker Compose files to /export/kingdezigns-all/Docker/Compose/
|
||
|
|
5. Restore app data to /export/kingdezigns-all/Docker/
|
||
|
|
6. Recreate containers: docker compose up -d (in each compose folder)
|
||
|
|
7. Restore crontab: crontab cron/root-crontab.txt
|
||
|
|
=============================================================
|
||
|
|
MANIFEST_EOF
|
||
|
|
|
||
|
|
log "Manifest written"
|
||
|
|
add_section_row "Backup Manifest" "OK" "MANIFEST.txt written to archive"
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# SECTION 7: CREATE ARCHIVE
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
log_section "Creating compressed archive"
|
||
|
|
|
||
|
|
cd "$TMP_BASE"
|
||
|
|
if tar -czf "${BACKUP_ROOT}/${ARCHIVE_NAME}" -C "$TMP_BASE" "$(basename "$TMP_DIR")" 2>/dev/null; then
|
||
|
|
log "Archive created: ${BACKUP_ROOT}/${ARCHIVE_NAME}"
|
||
|
|
ARCHIVE_SIZE=$(du -sh "${BACKUP_ROOT}/${ARCHIVE_NAME}" | cut -f1)
|
||
|
|
log "Archive size: ${ARCHIVE_SIZE}"
|
||
|
|
add_section_row "Archive Creation" "OK" "${ARCHIVE_NAME} · ${ARCHIVE_SIZE}"
|
||
|
|
else
|
||
|
|
log "ERROR: Archive creation failed"
|
||
|
|
ARCHIVE_SIZE="N/A"
|
||
|
|
add_section_row "Archive Creation" "ERROR" "tar failed — archive not created"
|
||
|
|
rm -rf "$TMP_DIR"
|
||
|
|
OVERALL_STATUS="FAILED"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# SECTION 8: CLEANUP TEMP FILES
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
log_section "Cleaning up temporary files"
|
||
|
|
|
||
|
|
if rm -rf "$TMP_DIR" 2>/dev/null; then
|
||
|
|
log "Temporary directory removed"
|
||
|
|
else
|
||
|
|
log "WARNING: Failed to remove temp directory: $TMP_DIR"
|
||
|
|
fi
|
||
|
|
rmdir "$TMP_BASE" 2>/dev/null && log ".tmp directory removed (empty)" || true
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# SECTION 9: ROTATE OLD BACKUPS
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
log_section "Rotating old backups (keeping last ${RETAIN_COUNT})"
|
||
|
|
|
||
|
|
BACKUP_LIST=$(ls -1t "${BACKUP_ROOT}"/nas08-backup-*.tar.gz 2>/dev/null || true)
|
||
|
|
BACKUP_COUNT=$(echo "$BACKUP_LIST" | grep -c . || true)
|
||
|
|
DELETED_COUNT=0
|
||
|
|
|
||
|
|
if [ "$BACKUP_COUNT" -gt "$RETAIN_COUNT" ]; then
|
||
|
|
DELETE_LIST=$(echo "$BACKUP_LIST" | tail -n +$((RETAIN_COUNT + 1)))
|
||
|
|
while IFS= read -r OLD_BACKUP; do
|
||
|
|
rm -f "$OLD_BACKUP"
|
||
|
|
log "Removed old backup: $(basename "$OLD_BACKUP")"
|
||
|
|
DELETED_COUNT=$((DELETED_COUNT + 1))
|
||
|
|
done <<< "$DELETE_LIST"
|
||
|
|
add_section_row "Backup Rotation" "OK" "Kept $RETAIN_COUNT most recent · removed $DELETED_COUNT old archive(s)"
|
||
|
|
else
|
||
|
|
log "Backup count (${BACKUP_COUNT}) within retention limit — no rotation needed"
|
||
|
|
add_section_row "Backup Rotation" "OK" "$BACKUP_COUNT / $RETAIN_COUNT slots used — no rotation needed"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# DONE — log summary
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
log_section "Backup Complete"
|
||
|
|
log "Archive: ${BACKUP_ROOT}/${ARCHIVE_NAME}"
|
||
|
|
log "Size: ${ARCHIVE_SIZE:-N/A}"
|
||
|
|
log "Status: ${OVERALL_STATUS}"
|
||
|
|
log ""
|
||
|
|
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
# BUILD & SEND HTML EMAIL (pattern: nas08_zfs_scrub.sh)
|
||
|
|
# -----------------------------------------------------------------------------
|
||
|
|
|
||
|
|
# Overall banner & badge
|
||
|
|
case "$OVERALL_STATUS" in
|
||
|
|
SUCCESS)
|
||
|
|
BADGE_HTML="<div style=\"background:#1a7f4b;color:white;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:700;\">✅ SUCCESS</div>"
|
||
|
|
BANNER_HTML="<tr><td style=\"background:#1a7f4b;padding:12px 32px;\"><span style=\"color:white;font-size:14px;font-weight:600;\">✅ Backup completed successfully — no action required.</span></td></tr>"
|
||
|
|
;;
|
||
|
|
WARNING)
|
||
|
|
BADGE_HTML="<div style=\"background:#b45309;color:white;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:700;\">⚠️ WARNING</div>"
|
||
|
|
BANNER_HTML="<tr><td style=\"background:#b45309;padding:12px 32px;\"><span style=\"color:white;font-size:14px;font-weight:600;\">⚠️ Backup completed with warnings — review recommended.</span></td></tr>"
|
||
|
|
;;
|
||
|
|
FAILED)
|
||
|
|
BADGE_HTML="<div style=\"background:#b91c1c;color:white;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:700;\">🚨 FAILED</div>"
|
||
|
|
BANNER_HTML="<tr><td style=\"background:#b91c1c;padding:12px 32px;\"><span style=\"color:white;font-size:14px;font-weight:600;\">🚨 Backup failed — immediate attention required!</span></td></tr>"
|
||
|
|
;;
|
||
|
|
esac
|
||
|
|
|
||
|
|
# Archive info card — only shown when archive was created
|
||
|
|
if [[ "${ARCHIVE_SIZE:-N/A}" != "N/A" ]]; then
|
||
|
|
ARCHIVE_CARD="
|
||
|
|
<tr><td style=\"padding:0 24px 8px;\">
|
||
|
|
<div style=\"background:white;border-radius:10px;border:1px solid #e5e7eb;padding:18px 22px;\">
|
||
|
|
<div style=\"font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;\">🗜️ Archive</div>
|
||
|
|
<table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">
|
||
|
|
<tr>
|
||
|
|
<td style=\"width:50%;padding:8px 12px;background:#f9fafb;border-radius:6px;text-align:center;\">
|
||
|
|
<div style=\"font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;\">Filename</div>
|
||
|
|
<div style=\"font-family:monospace;font-size:13px;font-weight:600;color:#111827;margin-top:4px;\">$ARCHIVE_NAME</div>
|
||
|
|
</td>
|
||
|
|
<td style=\"width:4%;\"></td>
|
||
|
|
<td style=\"width:20%;padding:8px 12px;background:#f9fafb;border-radius:6px;text-align:center;\">
|
||
|
|
<div style=\"font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;\">Size</div>
|
||
|
|
<div style=\"font-size:20px;font-weight:700;color:#111827;margin-top:2px;\">$ARCHIVE_SIZE</div>
|
||
|
|
</td>
|
||
|
|
<td style=\"width:4%;\"></td>
|
||
|
|
<td style=\"width:20%;padding:8px 12px;background:#f9fafb;border-radius:6px;text-align:center;\">
|
||
|
|
<div style=\"font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;\">Retained</div>
|
||
|
|
<div style=\"font-size:20px;font-weight:700;color:#111827;margin-top:2px;\">$BACKUP_COUNT / $RETAIN_COUNT</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</td></tr>"
|
||
|
|
else
|
||
|
|
ARCHIVE_CARD=""
|
||
|
|
fi
|
||
|
|
|
||
|
|
# System versions card
|
||
|
|
VERSIONS_CARD="
|
||
|
|
<tr><td style=\"padding:0 24px 8px;\">
|
||
|
|
<div style=\"background:white;border-radius:10px;border:1px solid #e5e7eb;padding:18px 22px;\">
|
||
|
|
<div style=\"font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;\">🖥️ System Versions</div>
|
||
|
|
<table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">
|
||
|
|
<tr>
|
||
|
|
<td style=\"font-size:13px;color:#6b7280;padding:4px 0;\">OMV</td>
|
||
|
|
<td style=\"font-size:13px;color:#111827;font-weight:600;\">${OMV_VER}</td>
|
||
|
|
<td style=\"font-size:13px;color:#6b7280;padding:4px 0;\">Kernel</td>
|
||
|
|
<td style=\"font-size:13px;color:#111827;font-weight:600;\">${KERNEL_VER}</td>
|
||
|
|
</tr>
|
||
|
|
<tr><td colspan=\"4\" style=\"height:4px;\"></td></tr>
|
||
|
|
<tr>
|
||
|
|
<td style=\"font-size:13px;color:#6b7280;padding:4px 0;\">Docker</td>
|
||
|
|
<td style=\"font-size:13px;color:#111827;font-weight:600;\">${DOCKER_VER}</td>
|
||
|
|
<td style=\"font-size:13px;color:#6b7280;padding:4px 0;\">IP Address</td>
|
||
|
|
<td style=\"font-size:13px;color:#111827;font-weight:600;font-family:monospace;\">${HOST_IP}</td>
|
||
|
|
</tr>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</td></tr>"
|
||
|
|
|
||
|
|
# Compose full HTML email
|
||
|
|
HTML_BODY=$(cat <<EOF
|
||
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="en">
|
||
|
|
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
||
|
|
<body style="margin:0;padding:0;background:#f1f5f9;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
|
||
|
|
<table width="100%" cellspacing="0" cellpadding="0" style="background:#f1f5f9;padding:30px 0;">
|
||
|
|
<tr><td align="center">
|
||
|
|
<table width="680" cellspacing="0" cellpadding="0" style="max-width:680px;width:100%;">
|
||
|
|
|
||
|
|
<!-- Header -->
|
||
|
|
<tr><td style="background:#0f172a;border-radius:10px 10px 0 0;padding:28px 32px;">
|
||
|
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||
|
|
<tr>
|
||
|
|
<td>
|
||
|
|
<div style="font-size:11px;color:#94a3b8;letter-spacing:1px;text-transform:uppercase;">System Backup Monitor</div>
|
||
|
|
<div style="font-size:26px;font-weight:800;color:white;margin-top:4px;">💾 $HOSTNAME_LABEL</div>
|
||
|
|
<div style="font-size:13px;color:#94a3b8;margin-top:4px;">$REPORT_TIME | 7 sections · $WARNING_COUNT warning(s) · $ERROR_COUNT error(s)</div>
|
||
|
|
</td>
|
||
|
|
<td align="right">$BADGE_HTML</td>
|
||
|
|
</tr>
|
||
|
|
</table>
|
||
|
|
</td></tr>
|
||
|
|
|
||
|
|
<!-- Status banner -->
|
||
|
|
$BANNER_HTML
|
||
|
|
|
||
|
|
<!-- Section results table -->
|
||
|
|
<tr><td style="padding:28px 24px 8px;">
|
||
|
|
<div style="background:white;border-radius:10px;box-shadow:0 1px 6px rgba(0,0,0,.10);overflow:hidden;border:1px solid #e5e7eb;">
|
||
|
|
<div style="background:#f8fafc;padding:14px 22px;border-bottom:1px solid #e5e7eb;">
|
||
|
|
<span style="font-size:13px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:.5px;">📋 Backup Section Results</span>
|
||
|
|
</div>
|
||
|
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||
|
|
<tr style="background:#f9fafb;">
|
||
|
|
<th style="text-align:left;padding:8px 14px;font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;font-weight:600;border-bottom:1px solid #e5e7eb;width:30%;">Section</th>
|
||
|
|
<th style="text-align:left;padding:8px 14px;font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;font-weight:600;border-bottom:1px solid #e5e7eb;width:15%;">Status</th>
|
||
|
|
<th style="text-align:left;padding:8px 14px;font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;font-weight:600;border-bottom:1px solid #e5e7eb;">Detail</th>
|
||
|
|
</tr>
|
||
|
|
$SECTION_ROWS_HTML
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</td></tr>
|
||
|
|
|
||
|
|
<!-- Archive info card -->
|
||
|
|
$ARCHIVE_CARD
|
||
|
|
|
||
|
|
<!-- System versions card -->
|
||
|
|
$VERSIONS_CARD
|
||
|
|
|
||
|
|
<!-- Run summary card -->
|
||
|
|
<tr><td style="padding:0 24px 28px;">
|
||
|
|
<div style="background:white;border-radius:10px;border:1px solid #e5e7eb;padding:18px 22px;">
|
||
|
|
<div style="font-size:13px;font-weight:700;color:#374151;margin-bottom:10px;text-transform:uppercase;letter-spacing:.5px;">📊 Run Summary</div>
|
||
|
|
<table width="100%" cellspacing="0" cellpadding="0">
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Host</td>
|
||
|
|
<td style="font-size:13px;color:#111827;font-weight:600;">$HOSTNAME_LABEL</td>
|
||
|
|
</tr>
|
||
|
|
<tr><td colspan="2" style="height:6px;"></td></tr>
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Run Time</td>
|
||
|
|
<td style="font-size:13px;color:#111827;font-weight:600;">$REPORT_TIME</td>
|
||
|
|
</tr>
|
||
|
|
<tr><td colspan="2" style="height:6px;"></td></tr>
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Archive</td>
|
||
|
|
<td style="font-family:monospace;font-size:13px;color:#111827;font-weight:600;">${ARCHIVE_NAME}</td>
|
||
|
|
</tr>
|
||
|
|
<tr><td colspan="2" style="height:6px;"></td></tr>
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Archive Size</td>
|
||
|
|
<td style="font-size:13px;color:#111827;font-weight:600;">${ARCHIVE_SIZE:-N/A}</td>
|
||
|
|
</tr>
|
||
|
|
<tr><td colspan="2" style="height:6px;"></td></tr>
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Backups on Disk</td>
|
||
|
|
<td style="font-size:13px;color:#111827;font-weight:600;">$BACKUP_COUNT of $RETAIN_COUNT max retained</td>
|
||
|
|
</tr>
|
||
|
|
<tr><td colspan="2" style="height:6px;"></td></tr>
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Warnings</td>
|
||
|
|
<td style="font-size:13px;font-weight:700;color:$([ "$WARNING_COUNT" -gt 0 ] && echo "#b45309" || echo "#1a7f4b");">$WARNING_COUNT</td>
|
||
|
|
</tr>
|
||
|
|
<tr><td colspan="2" style="height:6px;"></td></tr>
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Errors</td>
|
||
|
|
<td style="font-size:13px;font-weight:700;color:$([ "$ERROR_COUNT" -gt 0 ] && echo "#b91c1c" || echo "#1a7f4b");">$ERROR_COUNT</td>
|
||
|
|
</tr>
|
||
|
|
<tr><td colspan="2" style="height:6px;"></td></tr>
|
||
|
|
<tr>
|
||
|
|
<td style="font-size:13px;color:#6b7280;">Overall Status</td>
|
||
|
|
<td style="font-size:13px;font-weight:700;color:$(
|
||
|
|
case "$OVERALL_STATUS" in
|
||
|
|
SUCCESS) echo "#1a7f4b" ;;
|
||
|
|
WARNING) echo "#b45309" ;;
|
||
|
|
*) echo "#b91c1c" ;;
|
||
|
|
esac
|
||
|
|
);">$OVERALL_STATUS</td>
|
||
|
|
</tr>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</td></tr>
|
||
|
|
|
||
|
|
<!-- Footer -->
|
||
|
|
<tr><td style="background:#0f172a;border-radius:0 0 10px 10px;padding:16px 32px;text-align:center;">
|
||
|
|
<span style="font-size:11px;color:#64748b;">Automated Backup Monitor | $HOSTNAME_LABEL | KingDezigns Infrastructure</span>
|
||
|
|
</td></tr>
|
||
|
|
|
||
|
|
</table>
|
||
|
|
</td></tr>
|
||
|
|
</table>
|
||
|
|
</body>
|
||
|
|
</html>
|
||
|
|
EOF
|
||
|
|
)
|
||
|
|
|
||
|
|
# Send email via OMV Postfix (Gmail relay)
|
||
|
|
SUBJECT="[Backup] $HOSTNAME_LABEL — $OVERALL_STATUS | $REPORT_TIME"
|
||
|
|
|
||
|
|
{
|
||
|
|
echo "To: $REPORT_TO"
|
||
|
|
echo "From: $REPORT_FROM"
|
||
|
|
echo "Subject: $SUBJECT"
|
||
|
|
echo "MIME-Version: 1.0"
|
||
|
|
echo "Content-Type: text/html; charset=UTF-8"
|
||
|
|
echo ""
|
||
|
|
echo "$HTML_BODY"
|
||
|
|
} | sendmail -t
|
||
|
|
|
||
|
|
log "Email sent to $REPORT_TO — Overall status: $OVERALL_STATUS"
|
||
|
|
log "===== NAS08 Backup script finished ====="
|