Descripción
Dos endpoints REST que exponen el motor de integridad F2:
- GET
/api/racks/integrity/overview— Radar en vivo (cacheado 60 s desde v1.159.0) + histórico 30 días + métrica gate. - POST
/api/racks/integrity/ack— Registra veredicto, silencia aviso, actualiza gate, invalida la caché del radar.
Ubicación: racks/api/integrity.py
Router: Registrado en config/urls.py con gating de módulo.
Versión: v1.60.0 (2026-07-17) · caché de 60 s desde v1.159.0 (25-09-2026).
GET /api/racks/integrity/overview
Propósito
Retorna estado completo del radar para una org: cómputo (en vivo o cacheado 60 s), histórico de snapshots diarios, y métrica de calibración del motor.
Firma
@router.get("/overview")
def overview(request) -> dict
Autenticación / Autorización
- Usuario: Autenticado (
@login_requiredimplícito en router protegido). - Organización: Miembro de la org (FK derivada de
request.user.organization). - Módulo:
integritydebe estar enorg.extra_modules(middleware gatea antes de llegar aquí). - Permiso: Ninguno adicional (lectura).
Respuesta (200 OK)
{
"result": {
"summary": {
"devices": 250,
"matched_alive": 248,
"matched_stale": 1,
"ip_conflict": 0,
"unobservable": 1,
"muted": 0,
"fidelity_pct": 99.2,
"coverage_pct": 99.5,
"undocumented": 3
},
"racks": [
{
"rack_id": 1,
"rack_name": "Rack-A",
"fidelity_pct": 100.0,
"coverage_pct": 100.0,
"matched_alive": 20,
"matched_stale": 0,
"ip_conflict": 0,
"unobservable": 0,
"muted": 0,
"findings": [
{
"device_id": 42,
"name": "Switch-A",
"u_position": 10,
"classification": "matched_stale",
"reason": "Plan says present, network cannot confirm (7+ days)",
"evidence": {
"device": {"name": "Switch-A", "ip": "10.0.1.10", "mac": "AA:BB:CC:DD:EE:FF"},
"profile": {"ip": null, "hostname": null, "last_seen": null},
"match_type": "exact"
},
"ack": {"verdict": "plan_updated", "muted_until": "2026-07-20T10:00:00Z", "note": "Ya retirado del rack"}
}
]
}
],
"undocumented": [
{"profile_id": 100, "ip": "10.0.1.50", "hostname": "scanner-lab", "vendor": "HP", "model": "OfficeJet", "device_type": "printer", "last_seen": "2026-07-17T14:30:00Z", "ack": null}
]
},
"history": [
{"date": "2026-07-10", "fidelity_pct": 98.5, "coverage_pct": 99.0},
{"date": "2026-07-17", "fidelity_pct": 99.2, "coverage_pct": 99.5}
],
"gate": {"verdicts": {"plan_updated": 45, "real_change": 50, "false_alarm": 5}, "signal": 95, "noise": 5, "total": 100, "signal_pct": 95.0}
}
Lógica (actualizada v1.159.0)
def overview(request):
require_perm(request, "racks", "view")
require_perm(request, "network", "view")
org = require_org(request)
# E1-06 (mega-auditoría): caché de 60 s por organización.
result = integrity.cached_integrity(org)
history = [...] # últimas 30 snapshots diarias, sin cambios
gate = integrity.gate_stats(org)
return {"result": result, "history": history, "gate": gate}
Antes de v1.159.0 llamaba directo a integrity.compute_integrity(org) en cada petición.
Códigos de estado
| Código | Caso |
|---|---|
| 200 | OK |
| 403 | Usuario no miembro de org, o módulo integrity no activo |
| 401 | No autenticado |
POST /api/racks/integrity/ack
Propósito
Registra veredicto del usuario sobre un hallazgo, silencia el aviso X días según el veredicto, invalida la caché del radar (v1.159.0) y retorna gate stats actualizado.
Firma
@router.post("/ack", response={200: dict, 400: dict, 404: dict})
def ack_finding(request, data: AckIn) -> (200, dict) | (400, dict) | (404, dict)
Request body
{"kind": "matched_stale", "verdict": "plan_updated", "device_id": 42, "profile_id": null, "note": "El switch fue retirado el 16/07"}
Schema AckIn:
class AckIn(Schema):
kind: str # matched_stale | ip_conflict | undocumented
verdict: str # plan_updated | real_change | false_alarm
device_id: int | None = None
profile_id: int | None = None
note: str = ""
Validación:
- Exactamente uno de
device_idoprofile_iddebe ser present. kindenintegrity.ACK_KINDS.verdictenintegrity.VERDICTS.- Si
device_id→ Device debe existir en la org del usuario. - Si
profile_id→ DeviceProfile debe existir en la org del usuario.
Autenticación / Autorización
- Usuario: Autenticado.
- Organización: Miembro de org.
- Módulo:
integrityactivo. - Permiso:
racks:editrequerido.
Lógica (fragmento, v1.159.0 añade la invalidación)
ack = IntegrityAck.objects.create(organization=org, device=device, profile=profile, kind=data.kind, verdict=data.verdict, note=data.note or "", subject_label=label[:200], muted_until=timezone.now() + timedelta(days=integrity.ACK_MUTE_DAYS[data.verdict]), created_by=request.user)
# E1-06: el veredicto cambia el radar (marca o silencia el hallazgo) — fuera la caché.
integrity.invalidate_overview(org.id)
Respuesta (200 OK)
{"ok": true, "ack_id": 1234, "muted_until": "2026-07-20T10:00:00Z", "gate": {"verdicts": {"plan_updated": 46, "real_change": 50, "false_alarm": 5}, "signal": 96, "noise": 5, "total": 101, "signal_pct": 95.0}}
Respuesta (400 Bad Request)
Casos: kind inválido, verdict inválido, no exactamente uno de device_id/profile_id.
Respuesta (404 Not Found)
Casos: Device con ese ID no existe en la org, DeviceProfile con ese ID no existe en la org.
Códigos de estado
| Código | Caso |
|---|---|
| 200 | OK, ack creado |
| 400 | Validación fallida |
| 404 | Device/Profile no encontrado en org |
| 403 | Falta módulo integrity o permiso racks:edit |
| 401 | No autenticado |
Caché de 60 s del radar — mega-auditoría ronda 4, E1-06 (25-09-2026, PR #607)
Cada carga del Observatory (pestaña Integrity) recalculaba compute_integrity entero: con la organización de demostración (~480 equipos) eso es una consulta pesada en cada GET, aunque el radar solo cambia cuando alguien reconoce un hallazgo o el inventario de racks/equipos se mueve.
racks/services/integrity.py (nuevo):
OVERVIEW_CACHE_SECONDS = 60.overview_cache_key(organization_id)→f"integrity:overview:{organization_id}".cached_integrity(organization)— lee de Valkey; si falla la lectura o no hay entrada, calcula concompute_integrityy guarda 60 s. Si Valkey falla, calcula en vivo: la caché nunca tumba el radar (solo loggea warning).invalidate_overview(organization_id)— borra la clave tras el commit (transaction.on_commit): cada petición va dentro de unatomic(middleware de RLS), y borrar antes del commit dejaría una lectura concurrente recalculando sobre datos que aún no están escritos, y volviendo a guardar el radar viejo 60 s más.on_rack_change/on_device_change— receptores depost_save/post_deletedeRack/Device, conectados enRacksConfig.ready()(racks/apps.py, nuevo).on_device_changeignora guardados cuyoupdate_fieldsno toque ningún campo que el radar lea (_DEVICE_FIELDS_READ:rack,rack_id,name,u_position,management_config) — así unupdate_fieldsdel deep discovery que solo toca puertos no invalida la caché.racks/services/rollup.py::invalidate_dashboard_rollupstambién llama ainvalidate_overview— el editor de racks guarda en lote (bulk_update/bulk_create), que no dispara señales de Django, así que necesita el mismo punto de invalidación que ya usaba para el rollup del dashboard.- El
POST /ackinvalida explícitamente tras crear elIntegrityAck(arriba).
E1-04 (mismo PR): compute_integrity ya no trae los campos JSON pesados de DeviceProfile (.defer(*DeviceProfile.HEAVY_JSON_FIELDS)) ni config/producer_state de MonitoringTarget — el radar no los usa, y en una org grande son los campos más caros de la fila.
Lo que NO invalida la caché: cambios en fichas del discovery (DeviceProfile) fuera de guardar/borrar Rack o Device, y el estado de monitorización en vivo — esos tardan como mucho OVERVIEW_CACHE_SECONDS (60 s) en reflejarse (decisión de Edu, T27). El cómputo directo (tarea diaria de snapshots, comando de gestión) no pasa por la caché — llama a compute_integrity sin envolver.
Tests: tests/racks/test_mega25_r4_integrity_cache.py (195 líneas).
Registro en config/urls.py
api.add_router("/racks/integrity", "racks.api.integrity.router") # Barra de Integridad F2 (module-gated, antes del CRUD /{rack_id})
Orden: Registrado ANTES de racks.api.library.router porque este último tiene /{rack_id} que captura todo. El comentario lo documenta.
Gating por módulo
El middleware check_module_access() en core/middleware.py intercepta requests a /api/racks/integrity/*:
MODULE_PREFIXES = {"/integrity/": "integrity", "/api/racks/integrity": "integrity"}
if request.path.startswith(prefix):
if not org.extra_modules.filter(slug="integrity").exists():
return 403
Resultado: Sin integrity en org.extra_modules, cualquier GET/POST → 403 Forbidden antes de llegar a overview() o ack_finding().
Ejemplos de uso (curl)
GET overview
curl -H "Authorization: Bearer $TOKEN" "https://api.crearacks.com/api/racks/integrity/overview"
POST ack
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"kind": "matched_stale", "verdict": "plan_updated", "device_id": 42, "note": "Switch retirado"}' \
"https://api.crearacks.com/api/racks/integrity/ack"
Véase también
- [[feature—racks—integridad-f2-radar-interno]]
- [[entity—racks—service—integrity-motor]]
- [[entity—racks—model—integrity-ack]]
- [[entity—racks—view—integrity-radar]]
- [[concept—core—module-registry]]