696 lines
30 KiB
Bash
696 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;\">✅ 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 — 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 | 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 "===== NAS16 Backup script finished ====="
|