Add backup/restore scripts and update web/admin UI

This commit is contained in:
ske087
2026-07-08 15:46:41 +03:00
parent f29dcd2e38
commit cac83f8aef
4 changed files with 998 additions and 27 deletions
+296
View File
@@ -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
+257
View File
@@ -0,0 +1,257 @@
#!/bin/bash
# =============================================================================
# Server Monitorizare v2 - Migration Restore Script
# =============================================================================
# Restores a backup archive produced by backup_system.sh onto a new server.
#
# Usage:
# ./scripts/restore_backup.sh <backup.tar.gz> [--db-only] [--ansible-only]
#
# What it does:
# 1. Extracts the archive to a temp staging area
# 2. Restores the SQLite database (with a safety backup of any existing DB)
# 3. Restores Ansible inventory, playbooks, ssh_keys, ansible.cfg
# 4. Restores WMT releases and ansible_settings.json
# 5. Fixes SSH key permissions (must be 600 for Ansible)
# 6. Prints post-restore checklist
#
# For a fresh server install, run this AFTER:
# pip install -r requirements.txt
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_DIR="$(dirname "$SCRIPT_DIR")"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
log() { echo "[$(date '+%H:%M:%S')] $*"; }
ok() { echo " [OK] $*"; }
warn() { echo " [WARN] $*"; }
err() { echo " [ERROR] $*" >&2; }
usage() {
echo "Usage: $0 <path/to/backup.tar.gz> [--db-only | --ansible-only]"
echo ""
echo "Options:"
echo " --db-only Restore only the SQLite database"
echo " --ansible-only Restore only Ansible files (inventory, playbooks, ssh_keys)"
echo ""
echo "Example:"
echo " $0 data/backups/smv2_backup_20260708_020000.tar.gz"
exit 1
}
# ---------------------------------------------------------------------------
# Parse args
# ---------------------------------------------------------------------------
ARCHIVE="${1:-}"
MODE="all" # all | db-only | ansible-only
YES=0 # set to 1 by --yes to skip interactive confirmation
[[ -n "$ARCHIVE" ]] || usage
[[ -f "$ARCHIVE" ]] || { err "Archive not found: $ARCHIVE"; exit 1; }
for arg in "${@:2}"; do
case "$arg" in
--db-only) MODE="db-only" ;;
--ansible-only) MODE="ansible-only" ;;
--yes|-y) YES=1 ;;
*) err "Unknown option: $arg"; usage ;;
esac
done
# ---------------------------------------------------------------------------
# Extract archive
# ---------------------------------------------------------------------------
log "Extracting archive: $ARCHIVE"
STAGE_DIR=$(mktemp -d)
trap 'rm -rf "$STAGE_DIR"' EXIT
tar -xzf "$ARCHIVE" -C "$STAGE_DIR"
log "Extracted to staging: $STAGE_DIR"
# Show manifest if present
if [[ -f "$STAGE_DIR/BACKUP_MANIFEST.txt" ]]; then
echo ""
echo "=== Backup Manifest ==="
cat "$STAGE_DIR/BACKUP_MANIFEST.txt"
echo "========================"
echo ""
fi
# ---------------------------------------------------------------------------
# Confirm before proceeding
# ---------------------------------------------------------------------------
if [[ "$YES" == "1" ]]; then
log "Non-interactive mode (--yes) — proceeding automatically."
else
read -r -p "Proceed with restore to $APP_DIR ? [y/N] " confirm
[[ "${confirm,,}" == "y" ]] || { log "Aborted."; exit 0; }
fi
# ---------------------------------------------------------------------------
# Restore: SQLite database
# ---------------------------------------------------------------------------
restore_db() {
local src="$STAGE_DIR/enhanced_monitoring.db"
local dst="$APP_DIR/data/enhanced_monitoring.db"
if [[ ! -f "$src" ]]; then
warn "No database file in archive — skipping DB restore"
return
fi
mkdir -p "$APP_DIR/data"
# Safety backup of any existing database
if [[ -f "$dst" ]]; then
local safety_bak="${dst}.pre_restore_${TIMESTAMP}.bak"
cp "$dst" "$safety_bak"
ok "Existing DB saved as safety backup: $(basename "$safety_bak")"
fi
cp "$src" "$dst"
ok "Database restored: $dst ($(du -sh "$dst" | cut -f1))"
# Quick sanity check
local dev_count; dev_count=$(sqlite3 "$dst" "SELECT COUNT(*) FROM devices;" 2>/dev/null || echo "?")
ok "Devices in restored DB: $dev_count"
}
# ---------------------------------------------------------------------------
# Restore: Ansible files
# ---------------------------------------------------------------------------
restore_ansible() {
local src="$STAGE_DIR/ansible"
if [[ ! -d "$src" ]]; then
warn "No ansible/ directory in archive — skipping"
return
fi
# Backup existing ansible dir if present
if [[ -d "$APP_DIR/ansible" ]]; then
local bak="$APP_DIR/ansible.pre_restore_${TIMESTAMP}.bak"
mv "$APP_DIR/ansible" "$bak"
ok "Existing ansible/ moved to: $(basename "$bak")"
fi
cp -a "$src" "$APP_DIR/ansible"
# Critical: SSH private key must be 600 or Ansible rejects it
if [[ -f "$APP_DIR/ansible/ssh_keys/app_key" ]]; then
chmod 600 "$APP_DIR/ansible/ssh_keys/app_key"
ok "SSH private key permissions set to 600"
fi
ok "Ansible directory restored"
ok "Playbooks: $(ls "$APP_DIR/ansible/playbooks/"*.yml 2>/dev/null | wc -l) files"
ok "Inventory hosts: $(grep -c 'ansible_host:' "$APP_DIR/ansible/inventory/dynamic_inventory.yaml" 2>/dev/null || echo 0)"
}
# ---------------------------------------------------------------------------
# Restore: WMT releases
# ---------------------------------------------------------------------------
restore_wmt_releases() {
local src="$STAGE_DIR/wmt_releases"
if [[ ! -d "$src" ]]; then return; fi
mkdir -p "$APP_DIR/data/wmt_releases"
# Only copy files not already present (don't overwrite newer releases)
for f in "$src"/*; do
local fname; fname=$(basename "$f")
if [[ ! -f "$APP_DIR/data/wmt_releases/$fname" ]]; then
cp "$f" "$APP_DIR/data/wmt_releases/"
ok "WMT release restored: $fname"
else
ok "WMT release already present (skipped): $fname"
fi
done
}
# ---------------------------------------------------------------------------
# Restore: ansible_settings.json
# ---------------------------------------------------------------------------
restore_settings() {
local src="$STAGE_DIR/ansible_settings.json"
if [[ ! -f "$src" ]]; then return; fi
local dst="$APP_DIR/data/ansible_settings.json"
if [[ -f "$dst" ]]; then
cp "$dst" "${dst}.pre_restore_${TIMESTAMP}.bak"
fi
cp "$src" "$dst"
ok "ansible_settings.json restored"
}
# ---------------------------------------------------------------------------
# Restore: config/
# ---------------------------------------------------------------------------
restore_config() {
local src="$STAGE_DIR/config"
if [[ ! -d "$src" ]]; then return; fi
if [[ -d "$APP_DIR/config" ]]; then
ok "config/ already present — skipping (manual merge may be needed)"
warn "Backup config is in: $STAGE_DIR/config/ (not auto-applied)"
return
fi
cp -a "$src" "$APP_DIR/config"
ok "config/ restored"
}
# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------
log "Restore mode: $MODE"
case "$MODE" in
all)
restore_db
restore_ansible
restore_wmt_releases
restore_settings
restore_config
;;
db-only)
restore_db
;;
ansible-only)
restore_ansible
restore_settings
;;
esac
# ---------------------------------------------------------------------------
# Post-restore checklist
# ---------------------------------------------------------------------------
echo ""
echo "================================================================"
echo " Post-Restore Checklist"
echo "================================================================"
echo " 1. Install Python dependencies (if fresh server):"
echo " cd $APP_DIR && pip install -r requirements.txt"
echo ""
echo " 2. Start the application:"
echo " cd $APP_DIR && python main.py"
echo ""
echo " 3. Verify device list loads at: http://<new-server-ip>:5000"
echo ""
echo " 4. Test Ansible connectivity:"
echo " cd $APP_DIR/ansible && ansible -i inventory/dynamic_inventory.yaml all -m ping --limit 1"
echo ""
echo " 5. If the new server IP has changed, update any WMT devices"
echo " pointing to the old server URL (wmt_global_config.server_log_url)"
echo " via the admin UI or:"
echo " sqlite3 data/enhanced_monitoring.db \\"
echo " \"UPDATE wmt_global_config SET server_log_url='http://<NEW_IP>:5000/api/log';\""
echo ""
echo " 6. Re-install cron backup on the new server:"
echo " bash $SCRIPT_DIR/backup_system.sh install-cron"
echo ""
echo "================================================================"
log "Restore completed."