Ubicación
racks/services/integrity.py (327 LOC, 0 __all__)
API Pública
compute_integrity(organization, fresh_days=7, target_fresh_minutes=30) → dict
Propósito: Computar el estado de integridad plano↔realidad de TODOS los racks de una organización.
Parámetros:
organization(Organization): la org a evaluarfresh_days(int, default 7): máx antigüedad del perfil de discovery para considerarlo “fresco”target_fresh_minutes(int, default 30): máx antigüedad del último check de monitorización
Retorno: Dict JSON-serializable con estructura:
{
"organization_id": 1,
"generated_at": "2026-07-17T12:30:00Z",
"fresh_days": 7,
"target_fresh_minutes": 30,
"racks": [
{
"rack_id": 1,
"rack_name": "A01",
"fidelity_pct": 95.2,
"coverage_pct": 87.5,
"counts": {
"matched_alive": 38,
"matched_stale": 1,
"ip_conflict": 0,
"unobservable": 5
},
"findings": [
{
"device_id": 123,
"name": "Router-Core-1",
"u_position": 2,
"classification": "matched_alive",
"reason": "Identidad conocida y evidencia fresca...",
"evidence": {
"management_ip": "10.0.1.5",
"profile_id": 456,
"profile_ip": "10.0.1.5",
"hostname": "router-core-1",
"vendor": "Cisco",
"serial_number": "FOC2245ABC123",
"mac_address": "00:11:22:33:44:55",
"profile_last_seen": "2026-07-15T08:00:00Z",
"target_id": 789,
"target_status": "up",
"target_last_check": "2026-07-17T12:10:00Z"
}
},
{
"device_id": 124,
"name": "Switch-Access-5",
"u_position": 8,
"classification": "matched_stale",
"reason": "El plano dice que está, pero la red no lo confirma (visto por última vez hace 15 días, umbral 7 días). Drift candidato: retirado, apagado o desconectado.",
"evidence": {
"management_ip": "10.0.2.50",
"profile_id": 457,
"profile_ip": "10.0.2.50",
"hostname": "switch-access-5",
"profile_last_seen": "2026-07-02T10:30:00Z"
}
}
]
}
],
"undocumented": [
{
"profile_id": 500,
"ip": "10.0.3.100",
"hostname": "unknown-device-x",
"vendor": "Juniper",
"model": "QFX5100",
"device_type": "switch",
"last_seen": "2026-07-16T18:45:00Z"
}
],
"summary": {
"racks": 3,
"devices": 127,
"matched_alive": 118,
"matched_stale": 2,
"ip_conflict": 1,
"unobservable": 6,
"undocumented": 4,
"fidelity_pct": 94.4,
"coverage_pct": 95.3
}
}
Solo lectura: no muta nada. El resultado es un snapshot temporal listo para persistir o ingerir.
persist_snapshots(organization, result, snapshot_date=None) → int
Propósito: Persistir el resultado de compute_integrity como fotos diarias en la tabla RackIntegritySnapshot.
Parámetros:
organization(Organization): la org propietariaresult(dict): salida decompute_integritysnapshot_date(date, default today): fecha de la foto
Retorno: Cantidad de filas creadas/actualizadas.
Comportamiento:
- Una fila por rack con counts + findings de ese rack
- Una fila con rack=NULL = resumen de organización (lleva summary + undocumented)
- Idempotente por (org, rack, fecha):
update_or_create→ llamar 2 veces el mismo día actualiza, no duplica
Sub-Componentes Internos
Funciones de Clasificación
_profile_is_fresh(profile, now, fresh_days) → bool
Determina si el perfil de discovery tiene evidencia fresca de vida.
- Si
profile=None: retorna False (equipo con IP pero nunca visto por el sensor) - Si existe: compara
max(last_verified, last_updated)contranow - timedelta(days=fresh_days) - Usado para
matched_alivey detección dematched_stale
_target_is_up_fresh(target, now, target_fresh_minutes) → bool
Determina si el monitoreo reporta estado “up” con chequeo reciente.
- Si
target=NoneOtarget.last_check=None: retorna False - Si
(now - target.last_check) > timedelta(minutes=target_fresh_minutes): retorna False - Si estado no es “up”: retorna False
- Usado como segunda señal de vida (alternativa a profile fresco)
_management_profile_id(device) → int | None
Extrae el profile_id embebido en device.management_config (JSON).
- El auto-provision almacena aquí el ID del perfil descubierto en forma confiable
- Necesario para enlazar equipo ↔ perfil incluso si la IP cambió
_evidence(device, mip, profile, target) → dict
Construye la evidencia de una clasificación: qué sabemos del dispositivo, su perfil y su monitorización.
- IP de gestión del plano
- Profile: ID, IP, hostname, vendor, serial, MAC, timestamps
- Target: ID, estado, timestamps
- Usado para auditoría manual del gate #202
Constantes
DEFAULT_FRESH_DAYS = 7 # Umbral defecto discovery
DEFAULT_TARGET_FRESH_MINUTES = 30 # Umbral defecto monitorización
CLASS_MATCHED_ALIVE = "matched_alive"
CLASS_MATCHED_STALE = "matched_stale"
CLASS_IP_CONFLICT = "ip_conflict"
CLASS_UNOBSERVABLE = "unobservable"
Ajustables por flag de comando; el gate de calibración (task #202) fija los definitivos pre-producción.
Lógica de Clasificación (en pseudocódigo)
para cada equipo en plano:
mip = device.management_ip
profile = (linked_device OR profile_id embedido OR IP match)
target = (device-linked OR IP match)
SI no mip AND no profile AND no target:
CLASS = unobservable
RAZON = "Plano sin identidad de red"
SINO SI profile AND mip AND profile.ip != mip:
CLASS = ip_conflict
RAZON = "Identidad enlazada vive en otra IP"
SINO SI otra IP en plano pertenece a profile enlazado a OTRO equipo:
CLASS = ip_conflict
RAZON = "IP del plano pertenece a otro equipo"
SINO SI profile-fresco OR target-up-fresco:
CLASS = matched_alive
RAZON = "Identidad conocida + evidencia fresca"
SINO:
CLASS = matched_stale
RAZON = "Identidad conocida pero sin evidencia fresca"
counts[CLASS] += 1
findings.append({device_id, classification, reason, evidence})
// Nivel org: perfiles vivos no documentados
para cada profile EN profiles NO USADO:
SI profile-fresco:
undocumented.append({ip, hostname, vendor, model, device_type, last_seen})
// Métricas finales
observable = total_devices - unobservable
fidelity = matched_alive / observable (%)
coverage = observable / total (%)
Dependencias
django.utils.timezone— timestamps de servidorracks.models.Rack— iteración de racks de orgnetwork.models.DeviceProfile— discoverymonitoring.models.MonitoringTarget— monitoreoracks.models.RackIntegritySnapshot— persistencia
Testing
Fichero: tests/racks/test_integrity.py (11 tests nuevos)
Cobertura:
- Clasificación correcta en cada montón
- Cálculo de fidelity/coverage
- Idempotencia de persist_snapshots
- RLS: snapshot visible solo en tenant correcto
- Edge cases: perfil=None, IP mutable, undocumented
Notas de Producto
- Sin UI todavía: F1 es motor internodel. Se expone vía comando (
rack_integrity_probe) y task diaria, no vía API. - Gate #202: antes de mostrar datos al usuario, validamos que ≥80% de los drifts detectados son cambios reales (usando
rack_integrity_probeen red de design partner). - Plan vivo: supercontext/barra-integridad/ en el workspace.
Véase también
- [[feature—racks—motor-integridad-f1]]
- [[entity—racks—model—rack-integrity-snapshot]]
- [[entity—racks—model—device]]
- [[entity—network—model—device-profile]]
- [[entity—monitoring—model—monitoring-target]]