CreaRack-SL

Servicio monitoring · provisioning — auto-provisionamiento de objetivos SNMP

Descripción

Módulo centralizado de auto-provisionamiento del Observatory: encapsula la lógica de descubrir DeviceProfile sin MonitoringTarget asociado y crear los objetivos correspondientes (idempotente mediante bulk_create(ignore_conflicts=True)).

Antes: la lógica estaba duplicada (~110 LOC casi idéntica) en las 4 vistas HTMX del Observatory (wireless_view, observatory_view, ups_view, signage_view). Ahora vive aquí, permitiendo que el caller gate la escritura detrás de observatory:edit.

Archivo: monitoring/services/provisioning.py (89 LOC públicas)
Introducido en: commit f263a03 (sesión s108, auditoría sa6)

API Pública

provision_profile_targets(org, profiles, scope) → None

Auto-provisiona un MonitoringTarget para cada entrada en profiles que:

  • Tenga un ip_address válido (no nulo).
  • No tenga ya linked_monitoring_target.

Parámetros:

  • org: Organization — organización propietaria de los objetivos creados (non-null).
  • profiles: List[DeviceProfile] — perfiles de dispositivo a vincular.
  • scope: str — alcance del objetivo ('wireless', 'ups', 'signage', 'network'). Se usa para filtrar objetivos existentes y asignar a los nuevos.

Comportamiento:

  1. Busca objetivos existentes en la org + scope con un único filter(organization=org, scope=scope).
  2. Itera los perfiles: para cada uno sin linked_monitoring_target, crea un nuevo MonitoringTarget con:
    • ip_address del perfil.
    • name = hostname del perfil o IP en fallback.
    • ping_enabled=True.
    • snmp_enabled = verdadero solo si el perfil tiene credenciales SNMP.
    • http_enabled=False.
    • enabled=True.
    • config = dict SNMP compilado por build_snmp_config(profile).
    • device = device vinculado del perfil (si existe).
  3. Persiste con bulk_create(new_targets, ignore_conflicts=True) — idempotente: si el objetivo ya existe (por una carrera concurrente), se ignora sin error.
  4. Re-busca objetivos recién creados y vincula sus PKs (asignado por la BD) a los perfiles.
  5. Actualiza todos los perfiles con bulk_update(profiles_to_link, ["linked_monitoring_target"]).

Fuente de datos:

  • Input: DeviceProfile (modelo network.models.DeviceProfile)
  • Escribe: MonitoringTarget (modelo monitoring.models.MonitoringTarget)
  • Lee: existentes de MonitoringTarget (single scan, O(n) en memoria)

Restricciones de seguridad:

  • DEBE llamarse solo si el usuario tiene permiso observatory:edit (no validado aquí; responsabilidad del caller, típicamente monitoring/views.py).
  • org debe ser no-nulo (valida antes de llamar).

Idempotencia: sí. Múltiples llamadas sucesivas con los mismos perfiles y scope no crean duplicados; perfil con linked_monitoring_target ya seteado se salta.

build_snmp_config(profile) → dict

Arma el dict de configuración SNMP desde las credenciales de un DeviceProfile.

Parámetros:

  • profile: DeviceProfile — perfil a convertir.

Devuelve: dict con claves:

  • snmp_community (si existe en el perfil).
  • snmp_version, snmp_v3_username, snmp_v3_auth_protocol, snmp_v3_auth_key, snmp_v3_priv_protocol, snmp_v3_priv_key (si v3 y credenciales presentes).

Propósito: centralizar la compilación de config para evitar duplication entre vistas y hacer más fácil futuros cambios (p.ej., secretos cifrados, validación de claves).

Helpers privados

  • _profile_port_map (interno): mapa de alcance → puertos por defecto del perfil (usado antes, ahora centralizado aquí si se reutiliza entre vistas).

Decisiones de diseño

  1. Idempotencia mediante bulk_create(..., ignore_conflicts=True): si dos requests concurrentes detectan el mismo perfil sin objetivo, ambos crean, pero solo uno persiste (basado en índice único (organization, ip_address, scope)). El otro se ignora sin error.

  2. Escrituras en dos queries: primero bulk_create, luego re-scan y bulk_update. Esto es más eficiente que un loop de save() por perfil (lo que ocurría antes en las vistas).

  3. Vinculación de PKs post-bulk: tras bulk_create(..., ignore_conflicts=True), los PKs de nuevos objetivos no están siempre asignados (depende del backend). Se re-buscan con filter(organization=org, scope=scope, ip_address__in=created_ips) y se vinculan a los perfiles.

  4. Caller es responsable del gate de permisos: este servicio NO valida observatory:edit. Las vistas lo validan antes de llamar. Esto mantiene el módulo enfocado en la lógica de provisión.

Contexto: Broken Access Control auditado (sa6)

Antes de este cambio, cada una de las 4 vistas hacía if user.is_authenticated: (sin gate de rol) y luego llamaba a la lógica de provisión inline. Un usuario readonly podía provocar escrituras abriendo la página.

Ahora: la lógica de provisión está aquí, y el caller (vistas) valida has_permission(user, "observatory", "edit") antes de llamar. El modelo refuerza la garantía: la escritura no puede ocurrir sin permiso.

Véase también

  • [[feature—monitoring—auditoria-suprema-sa6-observatory-realtime]]
  • [[entity—monitoring—view—wireless]]
  • [[entity—monitoring—view—observatory]]
  • [[concept—security—role-based-access-control]]
  • [[concept—saas—multi-tenancy]]