223 lines
8.8 KiB
Python
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()
|
|
|
|
|