Volver a la wiki

Servicio de Integridad — Motor de clasificación plano↔realidad

Ubicación

racks/services/integrity.py (327 LOC, 0 __all__)

API Pública

compute_integrity(organization, fresh_days=7, target_fresh_minutes=30) → dict

Propósito: Computar el estado de integridad plano↔realidad de TODOS los racks de una organización.

Parámetros:

Retorno: Dict JSON-serializable con estructura:

{
  "organization_id": 1,
  "generated_at": "2026-07-17T12:30:00Z",
  "fresh_days": 7,
  "target_fresh_minutes": 30,
  "racks": [
    {
      "rack_id": 1,
      "rack_name": "A01",
      "fidelity_pct": 95.2,
      "coverage_pct": 87.5,
      "counts": {
        "matched_alive": 38,
        "matched_stale": 1,
        "ip_conflict": 0,
        "unobservable": 5
      },
      "findings": [
        {
          "device_id": 123,
          "name": "Router-Core-1",
          "u_position": 2,
          "classification": "matched_alive",
          "reason": "Identidad conocida y evidencia fresca...",
          "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"
          }
        },
        {
          "device_id": 124,
          "name": "Switch-Access-5",
          "u_position": 8,
          "classification": "matched_stale",
          "reason": "El plano dice que está, pero la red no lo confirma (visto por última vez hace 15 días, umbral 7 días). Drift candidato: retirado, apagado o desconectado.",
          "evidence": {
            "management_ip": "10.0.2.50",
            "profile_id": 457,
            "profile_ip": "10.0.2.50",
            "hostname": "switch-access-5",
            "profile_last_seen": "2026-07-02T10:30:00Z"
          }
        }
      ]
    }
  ],
  "undocumented": [
    {
      "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"
    }
  ],
  "summary": {
    "racks": 3,
    "devices": 127,
    "matched_alive": 118,
    "matched_stale": 2,
    "ip_conflict": 1,
    "unobservable": 6,
    "undocumented": 4,
    "fidelity_pct": 94.4,
    "coverage_pct": 95.3
  }
}

Solo lectura: no muta nada. El resultado es un snapshot temporal listo para persistir o ingerir.

persist_snapshots(organization, result, snapshot_date=None) → int

Propósito: Persistir el resultado de compute_integrity como fotos diarias en la tabla RackIntegritySnapshot.

Parámetros:

Retorno: Cantidad de filas creadas/actualizadas.

Comportamiento:

Sub-Componentes Internos

Funciones de Clasificación

_profile_is_fresh(profile, now, fresh_days) → bool

Determina si el perfil de discovery tiene evidencia fresca de vida.

_target_is_up_fresh(target, now, target_fresh_minutes) → bool

Determina si el monitoreo reporta estado “up” con chequeo reciente.

_management_profile_id(device) → int | None

Extrae el profile_id embebido en device.management_config (JSON).

_evidence(device, mip, profile, target) → dict

Construye la evidencia de una clasificación: qué sabemos del dispositivo, su perfil y su monitorización.

Constantes

DEFAULT_FRESH_DAYS = 7                    # Umbral defecto discovery
DEFAULT_TARGET_FRESH_MINUTES = 30        # Umbral defecto monitorización

CLASS_MATCHED_ALIVE = "matched_alive"
CLASS_MATCHED_STALE = "matched_stale"
CLASS_IP_CONFLICT = "ip_conflict"
CLASS_UNOBSERVABLE = "unobservable"

Ajustables por flag de comando; el gate de calibración (task #202) fija los definitivos pre-producción.

Lógica de Clasificación (en pseudocódigo)

para cada equipo en plano:
  mip = device.management_ip
  profile = (linked_device OR profile_id embedido OR IP match)
  target = (device-linked OR IP match)
  
  SI no mip AND no profile AND no target:
    CLASS = unobservable
    RAZON = "Plano sin identidad de red"
  
  SINO SI profile AND mip AND profile.ip != mip:
    CLASS = ip_conflict
    RAZON = "Identidad enlazada vive en otra IP"
  
  SINO SI otra IP en plano pertenece a profile enlazado a OTRO equipo:
    CLASS = ip_conflict
    RAZON = "IP del plano pertenece a otro equipo"
  
  SINO SI profile-fresco OR target-up-fresco:
    CLASS = matched_alive
    RAZON = "Identidad conocida + evidencia fresca"
  
  SINO:
    CLASS = matched_stale
    RAZON = "Identidad conocida pero sin evidencia fresca"
  
  counts[CLASS] += 1
  findings.append({device_id, classification, reason, evidence})

// Nivel org: perfiles vivos no documentados
para cada profile EN profiles NO USADO:
  SI profile-fresco:
    undocumented.append({ip, hostname, vendor, model, device_type, last_seen})

// Métricas finales
observable = total_devices - unobservable
fidelity = matched_alive / observable (%)
coverage = observable / total (%)

Dependencias

Testing

Fichero: tests/racks/test_integrity.py (11 tests nuevos)

Cobertura:

Notas de Producto

Véase también

Subir