Decisión
Lugar: MonitoringTarget.get_or_create_for_device + observatory_view
Principio: Un dispositivo cuya configuración de red es válida nunca puede tener monitoreo apagado, sin excepción.
Implementación: Re-encender MonitoringTarget.enabled=True en DOS puntos de reconciliación independientes para garantizar cobertura total.
Contexto
En el incidente s234, la malla de relaciones entre Device → management_config → MonitoringTarget.enabled se rompió:
- Device de prueba se guardó con IP de un AP real
- Validación falló →
enabled=Falsepor arrastre - Device se eliminó
- Target quedó en “estado zombi”: apagado, pero sin triggers que lo re-encendieran
El bug no fue “datos corruptos en BD”, sino falta de enforcement de una invariante de negocio.
Invariante
∀ device ∈ Device, ∀ target ∈ MonitoringTarget:
device.management_config.enabled ∧ device.management_config.valid
⟹ target.enabled = True
En lenguaje natural:
Si un dispositivo tiene una configuración de gestión de red válida y activa, su
MonitoringTargetdebe estar activo.
Implementación: DOS reconciliadores
1. MonitoringTarget.get_or_create_for_device (modela el flujo “hotpath”)
Se llama cuando:
- Se abre la ficha de un device
- Se actualiza su red
- Se sincroniza desde el Agente
# monitoreo/models.py, línea ~217
if existing:
update_fields = []
if existing.ip_address != ip:
existing.ip_address = ip
update_fields.append("ip_address")
# Invariante: gestión válida → target activo
if not existing.enabled:
existing.enabled = True
update_fields.append("enabled")
if update_fields:
existing.save(update_fields=update_fields)
return existing, False
Cubierto: Búsqueda por FK (device_id) y búsqueda por IP (Ficha Central).
2. observatory_view (batch “cold-path”)
Se ejecuta cuando el usuario abre Observatory (/monitoring/), reconcilia estado en lote:
# monitoring/views.py, línea ~196
if existing:
if existing.ip_address != ip or not existing.enabled:
existing.ip_address = ip
existing.enabled = True # ← Enforcing invariante en el batch
obs_targets_to_update.append(existing)
# ...
if obs_targets_to_update:
MonitoringTarget.objects.bulk_update(
obs_targets_to_update,
["ip_address", "device", "name", "enabled"] # ← enabled en la lista
)
Cubierto: Batch de devices → targets en Observatory, garantiza consistency antes de renderizar.
Por qué DOS lugares (y no uno)
| Scenario | get_or_create_for_device | observatory_view | Resultado |
|---|---|---|---|
| Device se actualiza vía API | ✅ | — | ✓ Re-enable inmediato |
| Usuario abre Observatory con target apagado | — | ✅ | ✓ Re-enable en batch |
| Device se borra pero target queda (zombi) | ✅ (si se sincroniza de nuevo) | ✅ | ✓ Double-check |
| Transición de IP por rebalanceo | ✅ | ✅ | ✓ Redundancia |
La redundancia es intencional: no confiar en que un solo punto de entrada ejecute. En un sistema SaaS con múltiples orígenes de verdad (API, Agente, UI, batch), es mejor sobre-enforcement que bajo-enforcement.
Tests que validan la invariante
test_get_or_create_reenables_zombie
device, target = self._zombie(organization, rack, ip="192.168.42.7")
# target.enabled = False (estado zombi)
found, created = MonitoringTarget.get_or_create_for_device(device, organization)
assert created is False
assert found.id == target.id
target.refresh_from_db()
assert target.enabled is True # ← Invariante enforced
test_observatory_view_reenables_zombie
_, target = self._zombie(organization, rack, ip="192.168.42.8")
# target.enabled = False
resp = authenticated_client.get("/monitoring/")
assert resp.status_code == 200
target.refresh_from_db()
assert target.enabled is True # ← Invariante enforced por el batch
Impacto arquitectónico
Posibles transiciones de estado
device.management_config.valid=False
└─> MonitoringTarget creado con enabled=False (OK, intención)
└─> device.management_config.valid=True (cambio de IP, reconfig)
└─> get_or_create_for_device: enabled → True (invariante)
└─> observatory_view: enabled → True (redundancia)
device delete() mientras target.enabled=False
└─> target queda "huérfano" (device_id=NULL)
└─> Si alguna lógica intenta re-ligar por IP → invariante enforced
└─> Si no, el target puede quedar zombi
└─> pero observatory_view en el siguiente reload: enabled → True
Cambio mínimo en schema
Solo añade lógica a update_fields — no toca migraciones, no cambia el modelo.
Alternativas consideradas y rechazadas
| Alternativa | Pro | Contra | Decisión |
|---|---|---|---|
Señal pre_save en Device | Centralizado en un lugar | Lógico: modelo de Device no debería saber de MonitoringTarget | ✗ |
| Trigger en BD (PostgreSQL) | Garantizado a nivel SQL | Opaco, difícil de debuggear, no portable | ✗ |
| Un solo punto (get_or_create) | Simpler | No cubre Observatory batch, riesgo de regresión | ✗ |
| DOS reconciliadores independientes | Redundancia, cobertura total, debuggeable | Ligeramente más código | ✅ |
Monitoring de la invariante
Hoy: Tests de regresión (TestZombieTargetReenable).
Futuro:
- Log
[INFO] MonitoringTarget re-enabled by invariante enforcementen ambos lugares - Métrica
monitoring.targets.reenabled_by_invariante_per_day - Alerta si la métrica sube anormalmente (podría indicar nuevo root cause)
Véase también
- [[incident—20260723—target-monitoreo-zombi]]
- [[entity—monitoring—model—monitoring-target]]
- [[entity—monitoring—endpoint—observatory-view]]
- [[concept—saas—data-consistency]]
- [[concept—monitoring—observability]]