Volver a la wiki

Endpoints API: GET/overview y POST/ack (Integridad)

Descripción

Dos endpoints REST que exponen el motor de integridad F2:

  1. GET /api/racks/integrity/overview — Radar en vivo (cacheado 60 s desde v1.159.0) + histórico 30 días + métrica gate.
  2. 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

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ódigoCaso
200OK
403Usuario no miembro de org, o módulo integrity no activo
401No 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:

Autenticación / Autorización

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ódigoCaso
200OK, ack creado
400Validación fallida
404Device/Profile no encontrado en org
403Falta módulo integrity o permiso racks:edit
401No 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):

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

Subir