nas16-scripts/omv/nas16-backup.sh
2026-07-26 21:21:03 -04:00

695 lines
30 KiB
Bash

#!/bin/bash
# =============================================================================
# NAS16 System Backup Script
# =============================================================================
# Backs up OMV config, system /etc, web server configs (Apache/PHP),
# MariaDB/MySQL databases, Webmin config, and scheduled jobs.
# 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).
#
# NOTE: Website files stored on NAS drives are NOT backed up here —
# they are covered separately via NAS-to-NAS redundancy.
#
# Schedule via OMV Scheduled Jobs (cron):
# 0 2 */3 * * (2:00 AM every 3 days)
#
# Backup destination: /export/kingdezignsnas-16/Backups/NAS16/
# Script location: /usr/scripts/omv/nas16-backup.sh
# =============================================================================
set -uo pipefail
# -----------------------------------------------------------------------------
# CONFIGURATION
# -----------------------------------------------------------------------------
HOSTNAME_LABEL=$(hostname -s 2>/dev/null || echo "NAS16")
REPORT_TO="rufus.king@kingdezigns.com"
REPORT_FROM="backup-monitor@nas16.local" # Display only — Postfix uses your Gmail relay
BACKUP_ROOT="/export/kingdezignsnas-16/Backups/NAS16"
TMP_BASE="/export/kingdezignsnas-16/Backups/.tmp"
DATE=$(date +%Y-%m-%d)
TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
REPORT_TIME=$(date "+%Y-%m-%d %H:%M:%S")
ARCHIVE_NAME="nas16-backup-${DATE}.tar.gz"
TMP_DIR="${TMP_BASE}/nas16-backup-${TIMESTAMP}"
RETAIN_COUNT=10
LOG_FILE="${BACKUP_ROOT}/backup.log"
# MariaDB/MySQL connection — uses OS root auth (no password needed when run as root)
DB_BACKUP_DIR_NAME="databases"
# -----------------------------------------------------------------------------
# STATUS TRACKING
# Each section appends a row to SECTION_ROWS_HTML and sets OVERALL_STATUS.
# 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 "NAS16 Backup Starting"
if [ ! -d "/export/kingdezignsnas-16" ]; then
log "ERROR: /export/kingdezignsnas-16 is not mounted or accessible. Aborting."
add_section_row "Pre-flight check" "ERROR" "/export/kingdezignsnas-16 not mounted or accessible"
# Send failure email immediately and exit
OVERALL_STATUS="FAILED"
# (email block at bottom handles this via trap — see END section)
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 — if present, export the structured XML as a bonus.
# The raw config.xml copy below is the primary backup and is fully sufficient on its own.
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: APACHE CONFIGURATION
# -----------------------------------------------------------------------------
log_section "Backing up Apache configuration"
APACHE_DIR="${TMP_DIR}/apache"
mkdir -p "$APACHE_DIR"
APACHE_WARN=0
for DIR in /etc/apache2/sites-available /etc/apache2/sites-enabled \
/etc/apache2/mods-enabled /etc/apache2/conf-available \
/etc/apache2/conf-enabled /etc/apache2; do
if [ -d "$DIR" ]; then
rsync -a --quiet "$DIR/" "${APACHE_DIR}/$(basename "$DIR")/" \
&& log "Apache $(basename "$DIR") backed up" \
|| { log "WARNING: Apache $(basename "$DIR") rsync encountered errors"; APACHE_WARN=$((APACHE_WARN+1)); }
else
log "WARNING: $DIR not found — skipping"
APACHE_WARN=$((APACHE_WARN+1))
fi
done
if [ -f /etc/apache2/apache2.conf ]; then
cp /etc/apache2/apache2.conf "${APACHE_DIR}/apache2.conf"
log "apache2.conf copied"
fi
if [ "$APACHE_WARN" -eq 0 ]; then
add_section_row "Apache Configuration" "OK" "All config directories backed up"
else
add_section_row "Apache Configuration" "WARNING" "$APACHE_WARN director(ies) missing or failed"
fi
# -----------------------------------------------------------------------------
# SECTION 4: PHP CONFIGURATION
# -----------------------------------------------------------------------------
log_section "Backing up PHP configuration"
PHP_DIR="${TMP_DIR}/php"
mkdir -p "$PHP_DIR"
PHP_FOUND=0
PHP_VERSIONS=""
PHP_WARN=0
for PHP_VER_DIR in /etc/php/*/; do
if [ -d "$PHP_VER_DIR" ]; then
PHP_VER=$(basename "$PHP_VER_DIR")
rsync -a --quiet "$PHP_VER_DIR" "${PHP_DIR}/${PHP_VER}/" \
&& log "PHP ${PHP_VER} config backed up" \
|| { log "WARNING: PHP ${PHP_VER} rsync encountered errors"; PHP_WARN=$((PHP_WARN+1)); }
PHP_FOUND=1
PHP_VERSIONS="${PHP_VERSIONS} ${PHP_VER}"
fi
done
if [ "$PHP_FOUND" -eq 0 ]; then
log "WARNING: No PHP config directories found under /etc/php/ — skipping"
add_section_row "PHP Configuration" "SKIPPED" "No PHP installations found under /etc/php/"
elif [ "$PHP_WARN" -gt 0 ]; then
add_section_row "PHP Configuration" "WARNING" "Versions found:${PHP_VERSIONS}$PHP_WARN version(s) had rsync errors"
else
add_section_row "PHP Configuration" "OK" "Versions backed up:${PHP_VERSIONS}"
fi
# -----------------------------------------------------------------------------
# SECTION 5: MARIADB / MYSQL DATABASES
# -----------------------------------------------------------------------------
log_section "Backing up MariaDB/MySQL databases"
DB_DIR="${TMP_DIR}/${DB_BACKUP_DIR_NAME}"
mkdir -p "$DB_DIR"
DB_COUNT=0
DB_FAIL=0
if command -v mysqldump &>/dev/null; then
DB_CLIENT="mysqldump"
elif command -v mariadb-dump &>/dev/null; then
DB_CLIENT="mariadb-dump"
else
log "WARNING: mysqldump/mariadb-dump not found — skipping database backup"
DB_CLIENT=""
fi
if [ -n "$DB_CLIENT" ]; then
DB_LIST=$(mysql --defaults-file=/etc/mysql/debian.cnf \
-e "SHOW DATABASES;" 2>/dev/null \
| grep -Ev "^(Database|information_schema|performance_schema|sys|mysql)$" \
|| true)
if [ -z "$DB_LIST" ]; then
log "No user databases found — nothing to dump"
add_section_row "MariaDB / MySQL" "SKIPPED" "No user databases found"
else
while IFS= read -r DB_NAME; do
[ -z "$DB_NAME" ] && continue
$DB_CLIENT \
--defaults-file=/etc/mysql/debian.cnf \
--single-transaction \
--routines \
--triggers \
--events \
"$DB_NAME" > "${DB_DIR}/${DB_NAME}.sql" \
&& { log "Database dumped: ${DB_NAME}.sql"; DB_COUNT=$((DB_COUNT+1)); } \
|| { log "WARNING: dump failed for database: ${DB_NAME}"; DB_FAIL=$((DB_FAIL+1)); }
done <<< "$DB_LIST"
log "Total databases dumped: ${DB_COUNT}"
gzip "${DB_DIR}"/*.sql 2>/dev/null && log "SQL dumps compressed" || true
if [ "$DB_FAIL" -eq 0 ]; then
add_section_row "MariaDB / MySQL" "OK" "$DB_COUNT database(s) dumped and compressed"
else
add_section_row "MariaDB / MySQL" "WARNING" "$DB_COUNT dumped · $DB_FAIL failed"
fi
fi
else
add_section_row "MariaDB / MySQL" "SKIPPED" "mysqldump / mariadb-dump not found"
fi
# -----------------------------------------------------------------------------
# SECTION 6: WEBMIN CONFIGURATION
# -----------------------------------------------------------------------------
log_section "Backing up Webmin configuration"
WEBMIN_DIR="${TMP_DIR}/webmin"
mkdir -p "$WEBMIN_DIR"
if [ -d /etc/webmin ]; then
if rsync -a --quiet /etc/webmin/ "${WEBMIN_DIR}/" 2>/dev/null; then
log "Webmin config backed up"
add_section_row "Webmin Configuration" "OK" "rsync from /etc/webmin completed"
else
log "WARNING: Webmin rsync encountered errors"
add_section_row "Webmin Configuration" "WARNING" "rsync completed with errors"
fi
else
log "WARNING: /etc/webmin not found — skipping (Webmin may not be installed)"
add_section_row "Webmin Configuration" "SKIPPED" "/etc/webmin not found — Webmin may not be installed"
fi
# -----------------------------------------------------------------------------
# SECTION 7: 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 8: BACKUP MANIFEST
# -----------------------------------------------------------------------------
log_section "Generating backup manifest"
APACHE_VER=$(apache2 -v 2>/dev/null | grep "Server version" | awk '{print $3}' || echo "Unknown")
PHP_VER_RUNTIME=$(php -r 'echo PHP_VERSION;' 2>/dev/null || echo "Unknown")
DB_VER=$(mysql --defaults-file=/etc/mysql/debian.cnf \
-e "SELECT VERSION();" 2>/dev/null | tail -1 || echo "Unknown")
WEBMIN_VER=$(dpkg -l webmin 2>/dev/null | awk '/^ii/{print $3}' || echo "Unknown")
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}')
MANIFEST="${TMP_DIR}/MANIFEST.txt"
cat > "$MANIFEST" <<MANIFEST_EOF
=============================================================
NAS16 BACKUP MANIFEST
=============================================================
Hostname: $(hostname)
Date: $(date)
Kernel: ${KERNEL_VER}
OMV Version: ${OMV_VER}
Apache: ${APACHE_VER}
PHP: ${PHP_VER_RUNTIME}
MariaDB/MySQL: ${DB_VER}
Webmin: ${WEBMIN_VER}
IP Address: ${HOST_IP}
CONTENTS:
omv-config/ - OMV exported config + raw config.xml
etc/ - Full /etc system configuration
apache/ - Apache2 virtual hosts, mods, site configs
php/ - PHP ini files and FPM pool configs (all versions)
databases/ - mysqldump of all user databases (.sql.gz per DB)
webmin/ - Webmin configuration (/etc/webmin)
cron/ - Root crontab and scheduled jobs
MANIFEST.txt - This file
NOT INCLUDED (by design):
Website files on NAS drives — covered by NAS-to-NAS redundancy
RESTORE NOTES:
1. Flash fresh Raspberry Pi OS Lite (64-bit) to SD card
2. SSH in and run OS update: sudo apt-get update && sudo apt-get upgrade -y
3. Install OMV via installScript
4. Enable PCIe in /boot/firmware/config.txt (after arm_boost=1):
dtparam=pciex1
dtparam=pciex1_gen=3
Then reboot.
5. Install ZFS: sudo apt-get install -y zfsutils-linux
6. Import ZFS pool: sudo zpool import -f kingdezignsnas-16
7. Extract backup archive to /tmp/nas16-restore/
8. Restore /etc: sudo rsync -a etc/ /etc/
9. Restore OMV config and run omv-salt deploy run all
10. Reboot — verify OMV loads correctly
11. Install Apache, PHP, MariaDB, Webmin, Adminer
12. Restore Apache config from apache/
13. Restore PHP config from php/
14. Restore all databases from databases/*.sql.gz
15. Restore Webmin config from webmin/
16. Restore crontab: sudo crontab cron/root-crontab.txt
17. Restart services: apache2, php-fpm, mariadb, webmin
18. Verify all websites load and databases are accessible
=============================================================
MANIFEST_EOF
log "Manifest written"
add_section_row "Backup Manifest" "OK" "MANIFEST.txt written to archive"
# -----------------------------------------------------------------------------
# SECTION 9: 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 10: 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 11: ROTATE OLD BACKUPS
# -----------------------------------------------------------------------------
log_section "Rotating old backups (keeping last ${RETAIN_COUNT})"
BACKUP_LIST=$(ls -1t "${BACKUP_ROOT}"/nas16-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;\">✅ &nbsp;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;\">⚠️ &nbsp;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;\">🚨 &nbsp;Backup failed — immediate attention required!</span></td></tr>"
;;
esac
# Archive info card — shown only 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
# Software 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;\">Apache</td>
<td style=\"font-size:13px;color:#111827;font-weight:600;\">${APACHE_VER}</td>
<td style=\"font-size:13px;color:#6b7280;padding:4px 0;\">PHP</td>
<td style=\"font-size:13px;color:#111827;font-weight:600;\">${PHP_VER_RUNTIME}</td>
</tr>
<tr><td colspan=\"4\" style=\"height:4px;\"></td></tr>
<tr>
<td style=\"font-size:13px;color:#6b7280;padding:4px 0;\">MariaDB / MySQL</td>
<td style=\"font-size:13px;color:#111827;font-weight:600;\">${DB_VER}</td>
<td style=\"font-size:13px;color:#6b7280;padding:4px 0;\">Webmin</td>
<td style=\"font-size:13px;color:#111827;font-weight:600;\">${WEBMIN_VER}</td>
</tr>
<tr><td colspan=\"4\" style=\"height:4px;\"></td></tr>
<tr>
<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>
<td></td><td></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 &nbsp;|&nbsp; 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 &nbsp;|&nbsp; $HOSTNAME_LABEL &nbsp;|&nbsp; 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 "===== NAS16 Backup script finished ====="