# 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()