rename to nettmanage, add NetBox upgrade function
This commit is contained in:
+1147
File diff suppressed because it is too large
Load Diff
-312
@@ -1,312 +0,0 @@
|
|||||||
#!/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)
|
|
||||||
Reference in New Issue
Block a user