#!/usr/bin/env python3 import os import sys import json import zipfile import subprocess import shutil import time from datetime import datetime # ========================================== # Standard Konfiguration # ========================================== CONFIG = { 'netbox_path': '/opt/netbox', 'db_host': 'localhost', 'db_name': 'netbox', 'db_user': 'netbox', 'db_pass': '', 'backup_dir': '/tmp' } # ========================================== # Hilfsfunktionen für die Shell-GUI # ========================================== class Colors: HEADER = '\033[95m' BLUE = '\033[94m' GREEN = '\033[92m' WARNING = '\033[93m' FAIL = '\033[91m' ENDC = '\033[0m' BOLD = '\033[1m' def clear_screen(): os.system('clear' if os.name == 'posix' else 'cls') def print_header(title): clear_screen() print(Colors.HEADER + Colors.BOLD + "=" * 60 + Colors.ENDC) print(Colors.HEADER + Colors.BOLD + f" {title}".ljust(59) + Colors.ENDC) print(Colors.HEADER + Colors.BOLD + "=" * 60 + Colors.ENDC + "\n") def log_info(msg): print(Colors.BLUE + f"[*] {msg}" + Colors.ENDC) def log_success(msg): print(Colors.GREEN + Colors.BOLD + f"[+] {msg}" + Colors.ENDC) def log_error(msg): print(Colors.FAIL + Colors.BOLD + f"[-] {msg}" + Colors.ENDC) def log_warn(msg): print(Colors.WARNING + f"[!] {msg}" + Colors.ENDC) def pause(): input(Colors.BOLD + "\nDrücke [ENTER], um ins Hauptmenü zurückzukehren..." + Colors.ENDC) # ========================================== # Kernfunktionen: Backup & Restore # ========================================== def execute_backup(): print_header("BACKUP PROZESS") log_info("Konfiguriere den Backup-Umfang:") do_db = input(" 1. Datenbank sichern? (J/n): ").strip().lower() != 'n' exclude_changelog = False exclude_users = False if do_db: exclude_changelog = input(Colors.BLUE + " -> Changelog (extras_objectchange) auslassen? Spart viel Platz. (j/N): " + Colors.ENDC).strip().lower() == 'j' print(Colors.WARNING + " Hinweis: Das Auslassen von Benutzern kann beim Restore zu Foreign-Key Fehlern führen, falls Objekte diesen Benutzern zugewiesen sind." + Colors.ENDC) exclude_users = input(Colors.BLUE + " -> Benutzer (User, Gruppen, Tokens) auslassen? (j/N): " + Colors.ENDC).strip().lower() == 'j' do_media = input("\n 2. Media-Dateien (Bilder/Dokumente) sichern? (J/n): ").strip().lower() != 'n' if not do_db and not do_media: log_warn("Keine Komponenten zum Sichern ausgewählt. Abbruch.") pause() return media_path = os.path.join(CONFIG['netbox_path'], "netbox/media") stamp = datetime.now().strftime("%Y%m%d_%H%M%S") archive_name = f"netbox_backup_{stamp}" zip_file_path = os.path.join(CONFIG['backup_dir'], f"{archive_name}.zip") tmp_dir = os.path.join(CONFIG['backup_dir'], f"nb_tmp_{stamp}") os.makedirs(tmp_dir, exist_ok=True) try: # 1. Datenbank-Dump if do_db: log_info("\nSchritt 1: Erstelle PostgreSQL Datenbank-Dump...") dump_file = os.path.join(tmp_dir, "netbox_database.dump") env = os.environ.copy() if CONFIG['db_pass']: env["PGPASSWORD"] = CONFIG['db_pass'] cmd = ["pg_dump", "-Fc", "-h", CONFIG['db_host'], "-U", CONFIG['db_user'], "-d", CONFIG['db_name'], "-f", dump_file] # Ausnahmen hinzufügen (--exclude-table-data lässt die Tabellenstruktur intakt, aber exportiert keine Inhalte) if exclude_changelog: cmd.append("--exclude-table-data=extras_objectchange") log_info(" -> Changelog-Daten werden übersprungen.") if exclude_users: cmd.extend(["--exclude-table-data=auth_user", "--exclude-table-data=auth_group", "--exclude-table-data=users_token"]) log_info(" -> Benutzer-Daten werden übersprungen.") process = subprocess.run(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) if process.returncode != 0: log_error(f"pg_dump fehlgeschlagen: {process.stderr}") raise Exception("Datenbank-Backup fehlgeschlagen.") log_success("PostgreSQL Dump erfolgreich erstellt.") else: log_info("\nSchritt 1: Datenbank-Backup übersprungen.") # 2. Media-Dateien if do_media: log_info("Schritt 2: Kopiere Media-Dateien (Bilder, Dokumente)...") if os.path.exists(media_path): dst_media = os.path.join(tmp_dir, "media") shutil.copytree(media_path, dst_media, dirs_exist_ok=True) log_success("Media-Ordner erfolgreich gesichert.") else: log_warn(f"Kein Media-Verzeichnis unter {media_path} gefunden.") else: log_info("Schritt 2: Media-Backup übersprungen.") # 3. Metadaten log_info("Schritt 3: Erstelle Metadaten...") meta = { "version": "1.1", "timestamp": stamp, "source_db": CONFIG['db_name'], "contains_db": do_db, "contains_media": do_media, "excluded_changelog": exclude_changelog, "excluded_users": exclude_users } with open(os.path.join(tmp_dir, "metadata.json"), "w") as f: json.dump(meta, f, indent=4) # 4. ZIP erstellen log_info(f"Schritt 4: Komprimiere Daten nach {zip_file_path} ...") with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED) as zipf: for root, _, files in os.walk(tmp_dir): for file in files: full_path = os.path.join(root, file) rel_path = os.path.relpath(full_path, tmp_dir) zipf.write(full_path, rel_path) print("\n" + "="*60) log_success("BACKUP ERFOLGREICH ABGESCHLOSSEN!") log_success(f"Datei liegt hier: {zip_file_path}") print("="*60) except Exception as e: log_error(f"Kritischer Fehler: {str(e)}") finally: if os.path.exists(tmp_dir): shutil.rmtree(tmp_dir) pause() def execute_restore(): print_header("RESTORE PROZESS") log_warn("ACHTUNG: Dieser Vorgang überschreibt bestehende Daten auf der Ziel-Instanz!") confirm = input("Bist du sicher, dass du fortfahren möchtest? (j/N): ") if confirm.lower() != 'j': log_info("Abbruch durch Benutzer.") pause() return restore_file = input(f"\nPfad zur Backup ZIP-Datei (z.B. /tmp/netbox_backup.zip): ").strip() if not restore_file or not os.path.exists(restore_file): log_error(f"Die Datei '{restore_file}' existiert nicht!") pause() return media_path = os.path.join(CONFIG['netbox_path'], "netbox/media") stamp = datetime.now().strftime("%Y%m%d_%H%M%S") tmp_extract_dir = f"/tmp/nb_restore_{stamp}" os.makedirs(tmp_extract_dir, exist_ok=True) try: log_info(f"\nSchritt 1: Entpacke {restore_file} ...") with zipfile.ZipFile(restore_file, 'r') as zipf: zipf.extractall(tmp_extract_dir) meta_file = os.path.join(tmp_extract_dir, "metadata.json") if not os.path.exists(meta_file): raise Exception("Ungültiges Backup: metadata.json fehlt.") with open(meta_file, 'r') as f: meta = json.load(f) # 2. Datenbank Restore if meta.get("contains_db", True): log_info("Schritt 2: Stelle PostgreSQL Datenbank wieder her...") dump_file = os.path.join(tmp_extract_dir, "netbox_database.dump") env = os.environ.copy() if CONFIG['db_pass']: env["PGPASSWORD"] = CONFIG['db_pass'] log_info("Lösche und erstelle Datenbank neu (Bereinigung)...") subprocess.run(["dropdb", "-h", CONFIG['db_host'], "-U", CONFIG['db_user'], "--if-exists", CONFIG['db_name']], env=env, stderr=subprocess.PIPE) subprocess.run(["createdb", "-h", CONFIG['db_host'], "-U", CONFIG['db_user'], "-O", CONFIG['db_user'], CONFIG['db_name']], env=env, stderr=subprocess.PIPE) log_info("Importiere Dump...") proc_restore = subprocess.run(["pg_restore", "-h", CONFIG['db_host'], "-U", CONFIG['db_user'], "-d", CONFIG['db_name'], "--clean", "--no-owner", dump_file], env=env, stderr=subprocess.PIPE, text=True) # pg_restore wirft oft Warnungen bei fehlenden Rechten, die aber meist unkritisch sind if proc_restore.returncode not in [0, 1]: log_warn(f"Import abgeschlossen mit Hinweisen (Code {proc_restore.returncode}). Dies kann passieren, wenn User exkludiert wurden.") log_success("Datenbank erfolgreich wiederhergestellt.") else: log_info("Schritt 2: Übersprungen (Keine Datenbank im Backup enthalten).") # 3. Media Restore if meta.get("contains_media", True): log_info("Schritt 3: Stelle Media-Dateien wieder her...") src_media = os.path.join(tmp_extract_dir, "media") if os.path.exists(src_media): if os.path.exists(media_path): shutil.rmtree(media_path) shutil.copytree(src_media, media_path, dirs_exist_ok=True) subprocess.run(["chown", "-R", "netbox:netbox", media_path], stderr=subprocess.PIPE) log_success("Media-Dateien wiederhergestellt.") else: log_info("Schritt 3: Übersprungen (Keine Media-Dateien im Backup enthalten).") # 4. NetBox Upgrade log_info("Schritt 4: Führe NetBox upgrade.sh aus (Cache-Clear & Migrations)...") upgrade_script = os.path.join(CONFIG['netbox_path'], "upgrade.sh") if os.path.exists(upgrade_script): subprocess.run([upgrade_script], stdout=subprocess.PIPE, stderr=subprocess.PIPE) log_success("Upgrade-Skript durchgelaufen.") # 5. Services Restart log_info("Schritt 5: Starte NetBox Dienste neu...") subprocess.run(["systemctl", "restart", "netbox", "netbox-rq"], stderr=subprocess.PIPE) print("\n" + "="*60) log_success("RESTORE ERFOLGREICH ABGESCHLOSSEN!") print("="*60) except Exception as e: log_error(f"Fehler beim Restore: {str(e)}") finally: if os.path.exists(tmp_extract_dir): shutil.rmtree(tmp_extract_dir) pause() def settings_menu(): while True: print_header("EINSTELLUNGEN") keys = list(CONFIG.keys()) for i, key in enumerate(keys, 1): val = "*****" if key == 'db_pass' and CONFIG[key] else CONFIG[key] print(f" {Colors.BOLD}{i}.{Colors.ENDC} {key.ljust(15)}: {Colors.BLUE}{val}{Colors.ENDC}") print(f"\n {Colors.BOLD}S.{Colors.ENDC} Speichern & Zurück zum Hauptmenü") choice = input("\nWelchen Wert möchtest du ändern? (Zahl/S): ").strip().lower() if choice == 's': break elif choice.isdigit() and 1 <= int(choice) <= len(keys): key_to_edit = keys[int(choice)-1] new_val = input(f"Neuer Wert für {key_to_edit} (aktuell: {CONFIG[key_to_edit]}): ") if new_val.strip() != "": CONFIG[key_to_edit] = new_val.strip() # ========================================== # Hauptmenü # ========================================== def main_menu(): while True: print_header("NETBOX BACKUP & RESTORE TOOL") print(f" {Colors.BOLD}1.{Colors.ENDC} Backup erstellen") print(f" {Colors.BOLD}2.{Colors.ENDC} Backup wiederherstellen") print(f" {Colors.BOLD}3.{Colors.ENDC} Einstellungen prüfen / anpassen") print(f" {Colors.BOLD}4.{Colors.ENDC} Beenden\n") choice = input(Colors.BOLD + "Wähle eine Aktion (1-4): " + Colors.ENDC).strip() if choice == '1': execute_backup() elif choice == '2': execute_restore() elif choice == '3': settings_menu() elif choice == '4': clear_screen() print("Beendet. Auf Wiedersehen!\n") sys.exit(0) else: log_error("Ungültige Eingabe.") time.sleep(1) if __name__ == "__main__": if os.geteuid() != 0: print(Colors.WARNING + "Hinweis: Dieses Skript sollte idealerweise als 'root' oder mit 'sudo' ausgeführt werden," + Colors.ENDC) print(Colors.WARNING + "da systemctl, chown und der Zugriff auf /opt/netbox erhöhte Rechte erfordern.\n" + Colors.ENDC) time.sleep(2) try: main_menu() except KeyboardInterrupt: print("\n\nAbbruch durch Benutzer. Ciao!") sys.exit(0)