Volver a la wiki

Modelo RackIntegritySnapshot — foto diaria de integridad plano↔realidad

Ubicación

racks/models.py (línea ~290)
Migraciones: 0014, 0015

Definición del Modelo

class RackIntegritySnapshot(models.Model):
    organization = ForeignKey(Organization, CASCADE, related_name="integrity_snapshots")
    rack = ForeignKey(Rack, CASCADE, null=True, blank=True, related_name="integrity_snapshots")
    date = DateField()
    
    fidelity_pct = FloatField(null=True, blank=True)
    coverage_pct = FloatField(null=True, blank=True)
    
    counts = JSONField(default=dict, blank=True)
    findings = JSONField(default=list, blank=True)
    
    created_at = DateTimeField(auto_now_add=True)
    updated_at = DateTimeField(auto_now=True)
    
    class Meta:
        constraints = [
            UniqueConstraint(
                fields=["organization", "rack", "date"],
                name="uniq_integrity_org_rack_date",
                nulls_distinct=False,
            )
        ]
        indexes = [
            Index(fields=["organization", "date"], name="idx_integrity_org_date")
        ]

Semántica

Una foto diaria del estado de integridad de un rack + una fila especial de resumen por organización.

CampoTipoNuloSignificado
organizationFK(Organization)NoPropietario (multi-tenancy scoping)
rackFK(Rack)SíIdentificador del rack. NULL = fila-resumen de org
dateDateNoFecha de la foto (YYYY-MM-DD, UTC)
fidelity_pctFloatSí% de equipos observable que cuadran con plano (0-100 o NULL si sin datos)
coverage_pctFloatSí% del plano que el sensor puede ver (0-100 o NULL si sin datos)
countsJSONSí (default {})Dict con conteos {matched_alive, matched_stale, ip_conflict, unobservable}
findingsJSONSí (default [])Array de hallazgos por dispositivo
created_atDateTimeNoTimestamp de inserción (auto_now_add)
updated_atDateTimeNoTimestamp de última actualización (auto_now)

Fila-Resumen (rack = NULL)

Cuando rack_id IS NULL, la fila representa el resumen de toda la organización:

Constraints & Indexes

UniqueConstraint uniq_integrity_org_rack_date

UniqueConstraint(
    fields=["organization", "rack", "date"],
    name="uniq_integrity_org_rack_date",
    nulls_distinct=False,
)

Importancia: nulls_distinct=False es crítico — permite que la fila-resumen (rack=NULL) sea única por org+fecha sin duplicados.

Impacto: RackIntegritySnapshot.objects.update_or_create(org=..., rack=None, date=..., defaults={...}) actualiza en vez de duplicar si se llama 2 veces el mismo día.

Index idx_integrity_org_date

Index(fields=["organization", "date"], name="idx_integrity_org_date")

Usado por:

Row-Level Security (RLS)

Migración 0015

Implementa el patrón core-0029: políticas USING + WITH CHECK idénticas.

ENABLE ROW LEVEL SECURITY ON racks_rackintegritysnapshot;
FORCE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON racks_rackintegritysnapshot
  USING (
    current_setting('app.current_org_id', true) IN ('0', '')  -- admin bypass
    OR organization_id = NULLIF(current_setting('app.current_org_id', true), '')::int
  )
  WITH CHECK (
    current_setting('app.current_org_id', true) IN ('0', '')
    OR organization_id = NULLIF(current_setting('app.current_org_id', true), '')::int
  );

Efecto:

Creación y Actualización

La tabla NUNCA se modifica directamente por usuario (no hay endpoint).

Se llena con racks.services.integrity.persist_snapshots():

# Opción 1: comando de calibración
python manage.py rack_integrity_probe --org 1 --save

# Opción 2: task diaria Huey (04:15 UTC)
compute_integrity_snapshots()  # en racks/tasks.py

Ambos llaman a persist_snapshots(org, result, snapshot_date) que hace:

for r in result["racks"]:
    RackIntegritySnapshot.objects.update_or_create(
        organization=organization,
        rack_id=r["rack_id"],
        date=snapshot_date,
        defaults={
            "fidelity_pct": r["fidelity_pct"],
            "coverage_pct": r["coverage_pct"],
            "counts": r["counts"],
            "findings": r["findings"],
        },
    )

# + fila-resumen (rack=NULL)
RackIntegritySnapshot.objects.update_or_create(
    organization=organization,
    rack=None,
    date=snapshot_date,
    defaults={
        "fidelity_pct": result["summary"]["fidelity_pct"],
        "coverage_pct": result["summary"]["coverage_pct"],
        "counts": ...,
        "findings": result["undocumented"],
    },
)

Esquema de JSON

counts (dict)

{
  "matched_alive": 38,
  "matched_stale": 1,
  "ip_conflict": 0,
  "unobservable": 5
}

Totales para este rack/org de cada clasificación.

findings (list of dict)

Para fila de rack:

[
  {
    "device_id": 123,
    "name": "Router-Core-1",
    "u_position": 2,
    "classification": "matched_alive",
    "reason": "Identidad conocida y evidencia fresca de vida (perfil reciente o monitorización up).",
    "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"
    }
  }
]

Para fila-resumen (rack=NULL):

[
  {
    "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"
  }
]

Perfiles descubiertos vivos pero sin documento en el plano.

Casos de Uso

1. Auditoría Manual del Gate #202

# Extraer snapshot más reciente
latest = RackIntegritySnapshot.objects.filter(
    organization=my_org,
    rack__isnull=False
).order_by("-date").first()

# Revisar avisos (stale/conflict) manualmente
for finding in latest.findings:
    if finding["classification"] in ("matched_stale", "ip_conflict"):
        print(f"Revisar: {finding['name']} — {finding['reason']}")

2. Series Temporales (F2/F3 Dashboard)

# Evolución de fidelity en un rack
snapshots = RackIntegritySnapshot.objects.filter(
    organization=my_org,
    rack=rack,
    date__gte=datetime(2026, 7, 1)
).order_by("date")

fidelities = [s.fidelity_pct for s in snapshots]
# → graficar evolución

3. Detección de Cambios Significativos

today = RackIntegritySnapshot.objects.get(org=org, rack=rack, date=today())
yesterday = RackIntegritySnapshot.objects.get(org=org, rack=rack, date=today()-timedelta(days=1))

if today.fidelity_pct < yesterday.fidelity_pct - 5:
    # caída significativa → alertar
    alert(f"Fidelity caló {yesterday.fidelity_pct}% → {today.fidelity_pct}%")

Relaciones de Navegación

Organization 1 ──── * RackIntegritySnapshot
Rack 1 ──── * RackIntegritySnapshot  (NULL para resumen org)

Notas de Implementación

Véase también

Subir