CreaRack-SL

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 de Organization.
  • device: Instancia de Device (opcional).
  • profile: Instancia de DeviceProfile (opcional).

Salida:

  • MonitoringTarget que será la FK de AIInsight.target.
  • None si el sujeto no tiene monitorización configurada.

Lógica:

  1. Query base: MonitoringTarget.objects.select_related("organization").filter(organization=organization).
    • select_related("organization") es crítico para evitar SynchronousOnlyOperation en contexto async.
  2. Si device is not None:
    • Busca target directo por device.
    • Si no existe, intenta por IP de gestión (device.management_ip).
  3. Si profile is not None:
    • Busca target por IP del perfil (profile.ip_address).
  4. 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 de IntegrityAck (contiene device_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:

  1. Llama a integrity.compute_integrity(organization) (costoso pero fresco).
  2. Busca el ack.device_id en results ["racks"][...]["findings"].
  3. Busca el ack.profile_id en results ["undocumented"].
  4. Construye label a partir de name, rack_name, u_position (device) u hostname/ip (profile).
  5. Preserva ack.subject_label si 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 del AIInsight creado o reutilizado.
  • None: Si el sujeto no tiene target (sin incidencia).

Flujo:

  1. Carga el ack con select_related para evitar N+1:

    ack = IntegrityAck.objects.select_related(
        "organization", "device", "device__rack", "profile"
    ).filter(id=ack_id).first()
  2. Guard checks:

    • Si ack is None o ack.verdict != VERDICT_REAL_CHANGE: retorna None.
    • Si ack.insight_id ya existe: retorna ese ID (idempotencia).
  3. 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
  4. Recomputa contexto:

    reason, evidence, label = _drift_context(org, ack)
    label = label or ack.subject_label or target.name
  5. 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."
    )
  6. 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(),
    )
  7. Vincula y persiste:

    ack.insight = insight
    ack.save(update_fields=["insight"])
  8. 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]]