Files
OnEverDrive/backend/app/scripts/backup_fortigate.py
T

223 lines
8.8 KiB
Python

# backup_fortigate.py
import os
import re
import sys
import subprocess
from datetime import datetime
import requests
import urllib3
# Desactivar advertencias de certificados SSL autofirmados
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Configuración FortiGate
FGT_HOST = "192.168.99.99"
FGT_PORT = "8443"
FGT_TOKEN = "bxtQkG7mymccqqc1wgwQyH7ngb4nbb"
FGT_BASE_URL = f"https://{FGT_HOST}:{FGT_PORT}"
# Configuración NAS / Ruta de Destino
# En Windows usará el UNC \\10.0.0.6\bak-fortigate
# En Linux (Debian LXC) usará /mnt/bak-fortigate (o lo que defina la variable de entorno NAS_PATH)
if os.name == 'nt':
DEFAULT_NAS_PATH = r"\\10.0.0.6\bak-fortigate"
else:
DEFAULT_NAS_PATH = "/mnt/bak-fortigate"
NAS_PATH = os.getenv("NAS_PATH", DEFAULT_NAS_PATH)
NAS_USER = "jenkins"
NAS_PASS = "LSJenkins2026*"
RETENTION_DAYS = 7 # Días a conservar en el NAS (7 días x 2 ejecuciones/día = 14 archivos)
def sanitize_filename(text: str) -> str:
"""Elimina caracteres inválidos para nombres de archivos."""
return re.sub(r'[\\/*?:"<>| ]', '_', text)
def authenticate_nas_share(path: str, username: str, password: str) -> bool:
"""Asegura la disponibilidad del recurso NAS en Windows o Linux (Debian)."""
print(f"[*] Verificando acceso al recurso NAS: {path}")
if os.path.exists(path):
print("[+] Conexión al recurso NAS activa y accesible.")
return True
# En Windows, intentar autenticación implícita con net use
if os.name == 'nt':
cmd = f'net use "{path}" "{password}" /user:"{username}"'
try:
res = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if res.returncode == 0 or os.path.exists(path):
print("[+] Conexión SMB establecida con éxito en Windows.")
return True
else:
print(f"[!] Advertencia 'net use': {res.stderr.strip()}")
except Exception as e:
print(f"[!] Error al ejecutar 'net use': {e}")
else:
# En Linux / Debian
print(f"[!] La ruta '{path}' no existe o no está montada.")
print(f"[*] Intentando crear el directorio local '{path}'...")
try:
os.makedirs(path, exist_ok=True)
if os.path.exists(path):
print("[+] Directorio creado/verificado exitosamente.")
return True
except Exception as e:
print(f"[!] No se pudo crear el directorio {path}: {e}")
return os.path.exists(path)
def cleanup_old_backups(directory_path: str, days_to_keep: int = 7):
"""
Elimina archivos de backup (.conf) en el directorio que superen los días de retención.
Con 2 ejecuciones diarias (06:00 y 18:00), se mantendrán hasta 14 archivos de los últimos 7 días.
"""
print(f"\n[*] Ejecutando limpieza de archivos antiguos (Retención: {days_to_keep} días)...")
if not os.path.exists(directory_path):
print(f"[!] La ruta {directory_path} no está disponible para limpieza.")
return
now = datetime.now()
cutoff_time = now.timestamp() - (days_to_keep * 86400)
deleted_count = 0
kept_count = 0
try:
files = [f for f in os.listdir(directory_path) if f.endswith(".conf")]
for file_name in files:
file_path = os.path.join(directory_path, file_name)
if not os.path.isfile(file_path):
continue
file_mtime = os.path.getmtime(file_path)
if file_mtime < cutoff_time:
try:
os.remove(file_path)
print(f" [-] Eliminado por antigüedad (> {days_to_keep} días): {file_name}")
deleted_count += 1
except Exception as err:
print(f" [!] Error al eliminar {file_name}: {err}")
else:
kept_count += 1
print(f"[+] Limpieza finalizada: {deleted_count} eliminado(s), {kept_count} conservado(s).")
except Exception as e:
print(f"[!] Error al escanear directorio de backups: {e}")
def get_fortigate_info(base_url: str, token: str):
"""Obtiene el modelo y la versión del firmware desde la API del FortiGate."""
url = f"{base_url}/api/v2/monitor/system/status"
headers = {"Authorization": f"Bearer {token}"}
print("[*] Consultando información del sistema FortiGate...")
try:
response = requests.get(url, headers=headers, verify=False, timeout=10)
if response.status_code == 200:
data = response.json()
results = data.get("results", {})
# Extraer modelo
model_name = results.get("model_name", "FortiGate")
model_number = results.get("model_number", "")
model = results.get("model", "")
if model_number:
full_model = f"{model_name}-{model_number}"
elif model:
full_model = f"{model_name}-{model}"
else:
full_model = model_name
# Extraer versión firmware y build
firmware_version = data.get("version", results.get("version", "vUnknown"))
build = data.get("build", results.get("build", ""))
if build:
full_version = f"{firmware_version}_b{build}"
else:
full_version = firmware_version
print(f"[+] Modelo detectado: {full_model}")
print(f"[+] Versión Firmware detectada: {full_version}")
return sanitize_filename(full_model), sanitize_filename(full_version)
else:
print(f"[!] No se pudo obtener info del sistema (HTTP {response.status_code}). Se usarán valores genéricos.")
except Exception as e:
print(f"[!] Error al consultar estado del sistema: {e}")
return "FortiGate", "vUnknown"
def download_backup(base_url: str, token: str) -> bytes:
"""Descarga la configuración del FortiGate mediante su API."""
url = f"{base_url}/api/v2/monitor/system/config/backup?scope=global"
headers = {"Authorization": f"Bearer {token}"}
print("[*] Solicitando backup de configuración a FortiGate...")
response = requests.get(url, headers=headers, verify=False, timeout=30)
if response.status_code == 200:
return response.content
elif response.status_code == 403:
print("\n" + "=" * 70)
print("[ERROR 403 - ACCESO PROHIBIDO EN FORTIGATE]")
print("El Token de API del usuario 'jenkins' no tiene permisos suficientes")
print("para descargar backups de configuración en el FortiGate.")
print("Solución en FortiGate: Ir a System > Admin Profiles, editar el perfil asignado")
print("a 'jenkins' y otorgar permisos 'Read/Write' o 'Read' en Maintenance / System Configuration.")
print("=" * 70 + "\n")
raise PermissionError("Permiso denegado (HTTP 403) en la API del FortiGate para realizar backups.")
else:
raise RuntimeError(f"Error al solicitar el backup. Código HTTP {response.status_code}: {response.text}")
def main():
print("=== INICIANDO RESPALDO DE FORTIGATE ===")
# 1. Autenticar y verificar conexión al NAS / Ruta Destino
if not authenticate_nas_share(NAS_PATH, NAS_USER, NAS_PASS):
print(f"[ERROR CRÍTICO] No se puede acceder a la ruta de destino: {NAS_PATH}")
sys.exit(1)
# 2. Obtener modelo y versión de Firmware
model, version = get_fortigate_info(FGT_BASE_URL, FGT_TOKEN)
# 3. Generar timestamp y nombre de archivo dinámico
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{model}_{version}_{timestamp}.conf"
destination_file = os.path.join(NAS_PATH, filename)
print(f"[*] Archivo destino configurado: {filename}")
# 4. Descargar backup
try:
backup_content = download_backup(FGT_BASE_URL, FGT_TOKEN)
except Exception as e:
print(f"[ERROR CRÍTICO] Falló la descarga del backup: {e}")
sys.exit(1)
# 5. Guardar backup en la carpeta compartida del NAS
try:
with open(destination_file, "wb") as f:
f.write(backup_content)
size_kb = len(backup_content) / 1024
print(f"\n[ÉXITO] Backup guardado exitosamente en NAS:")
print(f" Ruta: {destination_file}")
print(f" Tamaño: {size_kb:.2f} KB")
except Exception as e:
print(f"[ERROR CRÍTICO] Falló la escritura del archivo en el NAS: {e}")
sys.exit(1)
# 6. Limpieza de respaldos anteriores a 7 días
cleanup_old_backups(NAS_PATH, RETENTION_DAYS)
if __name__ == "__main__":
main()