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.
| Campo | Tipo | Nulo | Significado |
|---|---|---|---|
| organization | FK(Organization) | No | Propietario (multi-tenancy scoping) |
| rack | FK(Rack) | Sí | Identificador del rack. NULL = fila-resumen de org |
| date | Date | No | Fecha de la foto (YYYY-MM-DD, UTC) |
| fidelity_pct | Float | Sí | % de equipos observable que cuadran con plano (0-100 o NULL si sin datos) |
| coverage_pct | Float | Sí | % del plano que el sensor puede ver (0-100 o NULL si sin datos) |
| counts | JSON | Sí (default {}) | Dict con conteos {matched_alive, matched_stale, ip_conflict, unobservable} |
| findings | JSON | Sí (default []) | Array de hallazgos por dispositivo |
| created_at | DateTime | No | Timestamp de inserción (auto_now_add) |
| updated_at | DateTime | No | Timestamp 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:
counts= agregado de todos los racksfindings= array de perfilesundocumented(vivos en LAN sin plano)fidelity_pct= fidelidad globalcoverage_pct= cobertura global
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:
- Queries de series temporales: “dame todas las snapshots de org X desde fecha Y”
- Agregación diaria
- Dashboards (cuando lleguen en F2/F3)
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:
- Un request con GUC
app.current_org_id = '1'solo ve snapshots de org 1 - Bypass admin (GUC ‘0’ o ”) permite leer/escribir cualquier snapshot
- Used by:
TenantRLSMiddleware(HTTP requests), tasks de Huey (con GUC ‘0’ + scoping manual ORM)
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
- No visible en admin Django (sin registrar en admin.py) — datos internos
- Sin endpoints API — se accede vía comando o task; la UI llega en F2
- Timestamps UTC —
datees DateField (sin hora), ideales para series - JSON sin validación explícita — confía en que
persist_snapshotsgenera el schema correcto
Véase también
- [[feature—racks—motor-integridad-f1]]
- [[entity—racks—service—integrity]]
- [[entity—racks—model—rack]]
- [[entity—core—model—organization]]
- [[concept—saas—multi-tenancy]]
- [[decision—20260403—multi-tenancy-rls]]