Files
Server_Monitorizare_v2/scripts/backup_system.sh
T

297 lines
12 KiB
Bash
Executable File

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