Servicio: Puente cola de reconciliación → CNS/ITSM (integrity_itsm.py)
Servicio: Puente cola de reconciliación → CNS/ITSM (integrity_itsm.py)
Archivo: racks/services/integrity_itsm.py (112 LOC)
Módulo: racks.services
Introducido: v1.61.0 (2026-07-17)
Propósito
Mapear veredictos humanos (“Cambio real”) en la cola de reconciliación de Integridad a incidencias AIInsight en el CNS/ITSM, con validaciones de contexto y idempotencia.
Actúa como puente arquitectónico entre dos dominios:
- Racks: Integridad (planos ↔ red).
- Monitoring: ITSM (incidencias + observabilidad).
Funciones públicas
resolve_ticket_target(organization, *, device=None, profile=None) -> MonitoringTarget | None
Firma:
def resolve_ticket_target(organization, *, device=None, profile=None):
"""MonitoringTarget que ancla el ticket del sujeto, o None si no está monitorizado."""
Entrada:
organization: Instancia deOrganization.device: Instancia deDevice(opcional).profile: Instancia deDeviceProfile(opcional).
Salida:
MonitoringTargetque será la FK deAIInsight.target.Nonesi el sujeto no tiene monitorización configurada.
Lógica:
- Query base:
MonitoringTarget.objects.select_related("organization").filter(organization=organization).select_related("organization")es crítico para evitarSynchronousOnlyOperationen contexto async.
- Si
device is not None:- Busca target directo por
device. - Si no existe, intenta por IP de gestión (
device.management_ip).
- Busca target directo por
- Si
profile is not None:- Busca target por IP del perfil (
profile.ip_address).
- Busca target por IP del perfil (
- Devuelve el primer match o
None.
Uso:
target = resolve_ticket_target(org, device=device_obj)
if target:
open_drift_ticket(ack.id) # Crea incidencia
else:
# Sin target → sin incidencia, respuesta honesta
response_itsm = {"queued": False, "reason": "subject has no monitoring target"}
_drift_context(organization, ack) -> tuple[str | None, dict | None, str | None]
Propósito: Recomputa contexto fresco del sujeto desde el motor de Integridad (fuente única de verdad).
Entrada:
organization: Instancia.ack: Instancia deIntegrityAck(contienedevice_id,profile_id,subject_label).
Salida: Triple (reason, evidence, label).
reason: Texto de la anomalía (p.ej. “Alive on the network but missing from every rack plan.”).evidence: Dict con datos frescos (SNMP, perfiles undocumented, etc.).label: Identificador legible del sujeto (p.ej. “Router A @ Rack 5 U42”).
Devuelve (None, None, None) si el sujeto ya no aparece en el cómputo (p.ej. el perfil dejó de estar fresco entre click y task).
Lógica:
- Llama a
integrity.compute_integrity(organization)(costoso pero fresco). - Busca el
ack.device_iden results["racks"][...]["findings"]. - Busca el
ack.profile_iden results["undocumented"]. - Construye
labela partir dename,rack_name,u_position(device) uhostname/ip(profile). - Preserva
ack.subject_labelsi nada se encuentra (inmutabilidad del momento del click).
open_drift_ticket(ack_id: int) -> int | None
Propósito: Crea la incidencia AIInsight documental. Idempotente, condicional, async-safe.
Firma:
def open_drift_ticket(ack_id):
"""Abre el ticket ITSM de un veredicto real_change. Devuelve el insight id o None.
Idempotente: si el ack ya tiene ticket vinculado, no abre otro.
"""
Retorno:
int: ID delAIInsightcreado o reutilizado.None: Si el sujeto no tiene target (sin incidencia).
Flujo:
-
Carga el ack con
select_relatedpara evitar N+1:ack = IntegrityAck.objects.select_related( "organization", "device", "device__rack", "profile" ).filter(id=ack_id).first() -
Guard checks:
- Si
ack is Noneoack.verdict != VERDICT_REAL_CHANGE: retornaNone. - Si
ack.insight_idya existe: retorna ese ID (idempotencia).
- Si
-
Resuelve target:
target = resolve_ticket_target(org, device=ack.device, profile=ack.profile) if target is None: logger.info(f"[integrity] no drift ticket for ack {ack.id}: subject has no monitoring target") return None -
Recomputa contexto:
reason, evidence, label = _drift_context(org, ack) label = label or ack.subject_label or target.name -
Construye descripción para la IA:
description = ( f"Documentation drift confirmed by a human verdict in the rack integrity radar. " f"Subject: {label}. " f"{reason or 'The rack plan no longer matches the observed reality.'} " f"The resolution is to update the rack plan (or physically restore the documented state); " f"this is a documentation incident — no device commands should be executed." ) -
Llama async a
create_drift_insight():insight = async_to_sync(create_drift_insight)( target=target, anomaly_description=description, evidence=evidence or {}, subject_label=label, provider_name=get_default_provider_name(), ) -
Vincula y persiste:
ack.insight = insight ack.save(update_fields=["insight"]) -
Log:
logger.info(f"[integrity] drift ticket {insight.id} opened for ack {ack.id} ({label})")
Excepciones:
- Rate limit (de
create_drift_insight): Burbuja (la task Huey lo loguea). - Proveedor caído: Fallback automático a “static” rules en
create_drift_insight.
Modelo de datos
FK IntegrityAck.insight (migración racks 0018):
insight = models.ForeignKey(
"monitoring.AIInsight",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="integrity_acks",
)
- nullable: Acks sin target → sin insight.
- related_name:
AIInsight.integrity_acks(inversa). - on_delete: SET_NULL (preserva historial del ack aunque se expire el ticket).
Integración
De racks/api/integrity.py (endpoint POST /integrity/ack)
if data.verdict == integrity.VERDICT_REAL_CHANGE:
from racks.services.integrity_itsm import resolve_ticket_target
from racks.tasks import open_integrity_drift_ticket
if resolve_ticket_target(org, device=device, profile=profile):
open_integrity_drift_ticket(ack.id) # Encola en Huey
itsm = {"queued": True}
else:
itsm = {"queued": False, "reason": "subject has no monitoring target"}
return 200, {
"ok": True,
"ack_id": ack.id,
"muted_until": ack.muted_until.isoformat(),
"gate": integrity.gate_stats(org),
"itsm": itsm,
}
De racks/tasks.py (tarea Huey)
@db_task()
def open_integrity_drift_ticket(ack_id):
from racks.services import integrity_itsm
try:
insight_id = integrity_itsm.open_drift_ticket(ack_id)
if insight_id:
logger.info(f"[integrity] drift ticket {insight_id} for ack {ack_id}")
except Exception:
logger.exception(f"[integrity] drift ticket failed for ack {ack_id}")
Logging
Todos los eventos clave se loguean con prefijo [integrity] para facilitar debugging:
[integrity] no drift ticket for ack 42: subject has no monitoring target
[integrity] drift ticket 99 opened for ack 42 (Router A @ Rack 5 U42)
[integrity] integrity re-check armed (2 acks unmuted)
Véase también
- [[feature—racks—barra-integridad-f3]]
- [[entity—monitoring—service—create-drift-insight]]
- [[entity—racks—model—integrity-ack]]
- [[entity—monitoring—model—aiinsight]]
- [[entity—monitoring—model—monitoring-target]]