Add backup/restore scripts and update web/admin UI
This commit is contained in:
Executable
+296
@@ -0,0 +1,296 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# Server Monitorizare v2 - Backup System
|
||||
# =============================================================================
|
||||
# Backs up:
|
||||
# - SQLite database (safe online backup via sqlite3 .backup)
|
||||
# - Ansible inventory, playbooks, ansible.cfg, ssh_keys
|
||||
# - WMT releases and ansible_settings.json
|
||||
# - JSON export of critical device/config data (human-readable, portable)
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/backup_system.sh # run backup now
|
||||
# ./scripts/backup_system.sh install-cron # install daily cron job
|
||||
# ./scripts/backup_system.sh list # list existing backups
|
||||
# ./scripts/backup_system.sh verify <file> # verify a backup archive
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
APP_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
BACKUP_DIR="$APP_DIR/data/backups"
|
||||
DB_PATH="$APP_DIR/data/enhanced_monitoring.db"
|
||||
LOG_FILE="$APP_DIR/logs/backup.log"
|
||||
KEEP_DAILY=7 # keep last 7 daily backups
|
||||
KEEP_WEEKLY=4 # keep last 4 weekly backups (Sunday)
|
||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||
DAY_OF_WEEK=$(date +%u) # 1=Monday … 7=Sunday
|
||||
BACKUP_NAME="smv2_backup_${TIMESTAMP}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
log() {
|
||||
local level="$1"; shift
|
||||
local msg="$*"
|
||||
local ts; ts=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
echo "[$ts] [$level] $msg" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" &>/dev/null || { log ERROR "Required command not found: $1"; exit 1; }
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [backup|install-cron|list|verify <file>]"
|
||||
echo " (no argument) Run a full backup now"
|
||||
echo " install-cron Install daily cron job at 02:00"
|
||||
echo " list List existing backups with sizes"
|
||||
echo " verify <file> Verify integrity of a backup archive"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Subcommand: list
|
||||
# ---------------------------------------------------------------------------
|
||||
do_list() {
|
||||
echo "=== Existing backups in $BACKUP_DIR ==="
|
||||
if ls "$BACKUP_DIR"/*.tar.gz &>/dev/null; then
|
||||
ls -lh "$BACKUP_DIR"/*.tar.gz | awk '{print $5, $9}'
|
||||
else
|
||||
echo " (no .tar.gz backups found)"
|
||||
fi
|
||||
echo ""
|
||||
if ls "$BACKUP_DIR"/*.bak &>/dev/null; then
|
||||
echo "=== Legacy .bak files ==="
|
||||
ls -lh "$BACKUP_DIR"/*.bak | awk '{print $5, $9}'
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Subcommand: verify
|
||||
# ---------------------------------------------------------------------------
|
||||
do_verify() {
|
||||
local archive="$1"
|
||||
[[ -f "$archive" ]] || { echo "File not found: $archive"; exit 1; }
|
||||
echo "Verifying: $archive"
|
||||
tar -tzf "$archive" | head -30
|
||||
echo "..."
|
||||
echo "OK: archive appears intact ($(tar -tzf "$archive" | wc -l) entries)"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Subcommand: install-cron
|
||||
# ---------------------------------------------------------------------------
|
||||
do_install_cron() {
|
||||
local cron_line="0 2 * * * cd $APP_DIR && bash $SCRIPT_DIR/backup_system.sh >> $APP_DIR/logs/backup_cron.log 2>&1"
|
||||
# Check if already installed
|
||||
if crontab -l 2>/dev/null | grep -qF "backup_system.sh"; then
|
||||
echo "Cron job already installed:"
|
||||
crontab -l | grep "backup_system.sh"
|
||||
return
|
||||
fi
|
||||
# Append to crontab
|
||||
(crontab -l 2>/dev/null; echo "$cron_line") | crontab -
|
||||
echo "Cron job installed: daily at 02:00"
|
||||
echo " $cron_line"
|
||||
echo ""
|
||||
echo "To remove it later: crontab -e (and delete the backup_system.sh line)"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rotate old backups
|
||||
# ---------------------------------------------------------------------------
|
||||
rotate_backups() {
|
||||
log INFO "Rotating old backups (keep daily=$KEEP_DAILY, weekly=$KEEP_WEEKLY)..."
|
||||
|
||||
# Weekly backups: files tagged with "_weekly_" — rotate keeping last N
|
||||
local weekly_count
|
||||
weekly_count=$(find "$BACKUP_DIR" -maxdepth 1 -name 'smv2_backup_*_weekly_*.tar.gz' | wc -l)
|
||||
if (( weekly_count > KEEP_WEEKLY )); then
|
||||
find "$BACKUP_DIR" -maxdepth 1 -name 'smv2_backup_*_weekly_*.tar.gz' \
|
||||
| sort -r | tail -n +"$((KEEP_WEEKLY + 1))" \
|
||||
| xargs -r rm -v --
|
||||
log INFO "Removed old weekly backups (kept $KEEP_WEEKLY)"
|
||||
fi
|
||||
|
||||
# Daily backups: all other tar.gz files (not tagged _weekly_)
|
||||
local daily_count
|
||||
daily_count=$(find "$BACKUP_DIR" -maxdepth 1 -name 'smv2_backup_*.tar.gz' \
|
||||
| grep -v '_weekly_' | wc -l)
|
||||
if (( daily_count > KEEP_DAILY )); then
|
||||
find "$BACKUP_DIR" -maxdepth 1 -name 'smv2_backup_*.tar.gz' \
|
||||
| grep -v '_weekly_' | sort -r \
|
||||
| tail -n +"$((KEEP_DAILY + 1))" \
|
||||
| xargs -r rm -v --
|
||||
log INFO "Removed old daily backups (kept $KEEP_DAILY)"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Export critical tables to JSON (portable, readable by restore script)
|
||||
# ---------------------------------------------------------------------------
|
||||
export_json() {
|
||||
local export_dir="$1"
|
||||
log INFO "Exporting critical tables to JSON..."
|
||||
|
||||
python3 - "$DB_PATH" "$export_dir" <<'PYEOF'
|
||||
import sys, sqlite3, json, os
|
||||
|
||||
db_path = sys.argv[1]
|
||||
out_dir = sys.argv[2]
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
# Tables that are essential for migration (config + operational data)
|
||||
# Log/stats tables are excluded — they can be huge and are not needed on new server
|
||||
CRITICAL_TABLES = [
|
||||
'devices',
|
||||
'inventory_groups',
|
||||
'device_inventory_groups',
|
||||
'wmt_global_config',
|
||||
'wmt_update_requests',
|
||||
'message_templates',
|
||||
]
|
||||
|
||||
for table in CRITICAL_TABLES:
|
||||
try:
|
||||
rows = conn.execute(f"SELECT * FROM {table}").fetchall()
|
||||
data = [dict(r) for r in rows]
|
||||
with open(os.path.join(out_dir, f"{table}.json"), 'w') as f:
|
||||
json.dump(data, f, indent=2, default=str)
|
||||
print(f" Exported {table}: {len(data)} rows")
|
||||
except Exception as e:
|
||||
print(f" WARNING: could not export {table}: {e}")
|
||||
|
||||
conn.close()
|
||||
PYEOF
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main backup routine
|
||||
# ---------------------------------------------------------------------------
|
||||
do_backup() {
|
||||
require_cmd sqlite3
|
||||
require_cmd tar
|
||||
require_cmd python3
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
# Tag Sunday backups as weekly
|
||||
local tag=""
|
||||
if [[ "$DAY_OF_WEEK" == "7" ]]; then
|
||||
tag="_weekly"
|
||||
BACKUP_NAME="smv2_backup_${TIMESTAMP}${tag}"
|
||||
fi
|
||||
|
||||
STAGE_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$STAGE_DIR"' EXIT
|
||||
|
||||
log INFO "========================================================"
|
||||
log INFO "Starting backup: $BACKUP_NAME"
|
||||
log INFO "Stage dir: $STAGE_DIR"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 1. Safe SQLite backup (uses SQLite's online backup API — safe while
|
||||
# the application is running, produces a consistent snapshot)
|
||||
# ------------------------------------------------------------------
|
||||
log INFO "Backing up SQLite database..."
|
||||
sqlite3 "$DB_PATH" ".backup '$STAGE_DIR/enhanced_monitoring.db'"
|
||||
log INFO "Database backed up ($(du -sh "$STAGE_DIR/enhanced_monitoring.db" | cut -f1))"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 2. JSON export of critical tables
|
||||
# ------------------------------------------------------------------
|
||||
export_json "$STAGE_DIR/json_export"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 3. Ansible directory (inventory, playbooks, cfg, ssh_keys)
|
||||
# ssh_keys are included because ansible.cfg references them and
|
||||
# are needed for the new server to operate.
|
||||
# Permissions on ssh_keys (600) are preserved by tar.
|
||||
# ------------------------------------------------------------------
|
||||
log INFO "Backing up Ansible configuration..."
|
||||
cp -a "$APP_DIR/ansible" "$STAGE_DIR/ansible"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 4. ansible_settings.json (fallback SSH password, auth mode)
|
||||
# ------------------------------------------------------------------
|
||||
if [[ -f "$APP_DIR/data/ansible_settings.json" ]]; then
|
||||
cp "$APP_DIR/data/ansible_settings.json" "$STAGE_DIR/"
|
||||
log INFO "Backed up ansible_settings.json"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 5. WMT releases (zip packages used by playbooks)
|
||||
# ------------------------------------------------------------------
|
||||
if [[ -d "$APP_DIR/data/wmt_releases" ]] && \
|
||||
[[ -n "$(ls "$APP_DIR/data/wmt_releases/" 2>/dev/null)" ]]; then
|
||||
cp -a "$APP_DIR/data/wmt_releases" "$STAGE_DIR/wmt_releases"
|
||||
log INFO "Backed up wmt_releases ($(du -sh "$STAGE_DIR/wmt_releases" | cut -f1))"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 6. App config (excludes secrets already in ansible_settings.json)
|
||||
# ------------------------------------------------------------------
|
||||
cp -a "$APP_DIR/config" "$STAGE_DIR/config"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 7. Create manifest
|
||||
# ------------------------------------------------------------------
|
||||
cat > "$STAGE_DIR/BACKUP_MANIFEST.txt" <<EOF
|
||||
Server Monitorizare v2 - Backup Manifest
|
||||
=========================================
|
||||
Backup name : $BACKUP_NAME
|
||||
Created at : $(date '+%Y-%m-%d %H:%M:%S %Z')
|
||||
Hostname : $(hostname)
|
||||
DB size : $(du -sh "$STAGE_DIR/enhanced_monitoring.db" | cut -f1)
|
||||
DB devices : $(sqlite3 "$STAGE_DIR/enhanced_monitoring.db" "SELECT COUNT(*) FROM devices;")
|
||||
DB inv groups : $(sqlite3 "$STAGE_DIR/enhanced_monitoring.db" "SELECT COUNT(*) FROM inventory_groups;")
|
||||
WMT configs : $(sqlite3 "$STAGE_DIR/enhanced_monitoring.db" "SELECT COUNT(*) FROM wmt_global_config;")
|
||||
Ansible hosts : $(grep -c 'ansible_host:' "$STAGE_DIR/ansible/inventory/dynamic_inventory.yaml" 2>/dev/null || echo 0)
|
||||
|
||||
Included files:
|
||||
$(find "$STAGE_DIR" -type f | sed "s|$STAGE_DIR/||" | sort)
|
||||
EOF
|
||||
|
||||
log INFO "Manifest written"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 8. Compress everything into a single archive
|
||||
# ------------------------------------------------------------------
|
||||
local ARCHIVE="$BACKUP_DIR/${BACKUP_NAME}.tar.gz"
|
||||
log INFO "Compressing to $ARCHIVE ..."
|
||||
tar -czf "$ARCHIVE" -C "$STAGE_DIR" .
|
||||
local size; size=$(du -sh "$ARCHIVE" | cut -f1)
|
||||
log INFO "Archive created: $ARCHIVE ($size)"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 9. Rotate old backups
|
||||
# ------------------------------------------------------------------
|
||||
rotate_backups
|
||||
|
||||
log INFO "Backup complete: $ARCHIVE"
|
||||
log INFO "========================================================"
|
||||
echo ""
|
||||
echo "Backup complete: $ARCHIVE ($size)"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dispatch
|
||||
# ---------------------------------------------------------------------------
|
||||
case "${1:-backup}" in
|
||||
backup) do_backup ;;
|
||||
install-cron) do_install_cron ;;
|
||||
list) do_list ;;
|
||||
verify) [[ -n "${2:-}" ]] || usage; do_verify "$2" ;;
|
||||
help|--help|-h) usage ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user