#!/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 # 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 ]" 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 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" </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