Volver a la wiki

Servicio terminal_hosts — Registro unificado de hosts SSH

{“last_verified”: “2026-09-06T00:00:00Z”, “sources”: [{“type”:“code”,“ref”:“terminal/services.py”},{“type”:“code”,“ref”:“terminal/views.py”},{“type”:“code”,“ref”:“tests/api/test_terminal_hosts.py”},{“type”:“commit”,“ref”:“bf2c645”}], “related”: [“entity—terminal—endpoint—index”,“entity—racks—model—device”,“entity—network—model—device-profile”,“entity—network—model—monitoring-target”,“feature—terminal—todos-dispositivos-filtro-tipo”,“entity—monitoring—model—monitoring-target”], “content”: ”## Descripción\n\nterminal_hosts(org) es un servicio Python que normaliza y deduplica dos fuentes de dispositivos de red para la vista Terminal:\n\n1. racks.Device con Network Management habilitado (credenciales SSH guardadas) → sesión SSH directa al hacer clic.\n2. network.DeviceProfile detectados por Auto-Provision (wireless, UPS, Digital Signage, observatory) → conexión por modal manual con IP pre-rellena.\n\nSin modelo nuevo ni migración de BD. Reaprovecha registros que Auto-Provision ya crea.\n\n## Firma y ubicación\n\npython\n# terminal/services.py\n\ndef terminal_hosts(org):\n \"\"\"Lista unificada de hosts gestionables por Terminal.\n \n Returns: dict {\"hosts\": [...], \"racks\": [...]}\n \"\"\"\n\n\n## Componentes internos\n\n### 1. _device_hosts(org)\n\nPropósito: Extrae racks.Device con Network Management de la organización.\n\nFuentes:\n- Query: Device.objects.filter(rack__organization=org, rack__deleted_at__isnull=True)\n- Filtro adicional: has_network_management (property que decodifica el JSON de management_config)\n\nEstructura de cada host:\npython\n{\n \"uid\": \"device:<id>\", # Identificador único\n \"source\": \"device\", # Marca esta fuente\n \"category\": \"rack\", # Categoría para filtro UI\n \"device_id\": <int>,\n \"name\": str,\n \"ip\": str, # `management_ip`\n \"status\": str, # De `device.status`\n \"type_label\": str, # `management_vendor` o \"device\"\n \"derived_to\": \"Rack: <nombre>\", # Contexto visual\n \"rack_id\": <int>,\n \"group_ids_str\": \"id1,id2,...\", # IDs de grupos separados por coma\n}\n\n\nSalida adicional: taken_ips (set), racks_by_id (dict) para usar en el siguiente componente.\n\n---\n\n### 2. _profile_hosts(org, taken_ips)\n\nPropósito: Extrae network.DeviceProfile detectados, omitiendo IPs ya cubiertas por Device.\n\nFuentes:\n- Query: DeviceProfile.objects.filter(organization=org) — Hub Universal (D3): lista TODAS las fichas de la org, sin filtrar por SSH/SNMP.\n- Relaciones: linked_device__rack, linked_monitoring_target (select_related).\n- Deduplicación: Si ip_address está en taken_ips, se salta.\n\nLógica de categorización (Ficha Central: la decide DeviceProfile.assigned_page, no el MonitoringTarget — el campo scope del target fue eliminado en la migración monitoring/0025):\n- Si assigned_page ∈ {wireless, ups, signage} → category=assigned_page.\n- Si no está asignada pero tiene linked_device con rack → category=\"rack\".\n- Resto → category=\"other\".\n\nEtiqueta derived_to:\n- Con rack → \"Rack: <nombre>\".\n- Con assigned_page asignada → _PAGE_LABELS[assigned_page] (Wireless / UPS / Digital Signage).\n- Resto → \"Detected (unassigned)\".\n\nEstructura de cada host:\npython\n{\n \"uid\": \"profile:<id>\",\n \"source\": \"profile\", # Marca esta fuente\n \"category\": \"wireless|ups|signage|other|rack\",\n \"device_id\": None,\n \"name\": str, # `hostname` o fallback a `vendor model` o IP\n \"ip\": str, # `ip_address`\n \"status\": \"online\"|\"unknown\", # \"online\" si tiene creds SSH guardadas (has_stored_ssh) — CONECTABLE, no conectividad real. A propósito: lo usa el Terminal para decidir sesión directa vs modal (test_profile_with_stored_ssh_is_online_green, regresión intencional).\n \"connectivity_status\": str, # v1.119.0 (task #294): CONECTIVIDAD real — \"online\"/\"offline\"/\"unknown\" desde `status_effective` del `linked_monitoring_target` (\"up\"→online, \"down\"→offline, resto tal cual). Sin target vinculado → \"unknown\".\n \"type_label\": str, # `device_type` o `vendor` o \"device\"\n \"derived_to\": str, # Página asignada legible (assigned_page) o rack\n \"rack_id\": None,\n \"group_ids_str\": \"\", # Profiles no tienen grupos\n \"supports_ssh\": bool,\n}\n\n\nPor qué dos campos separados (task #294): el filtro “Status” de la página Devices usaba status (conectable por SSH) como si fuera conectividad — en PROD, 75 de 176 fichas decían “Online” mientras Observatory las tenía “down” en ese instante. connectivity_status es el campo que ahora usa ese filtro (templates/network/devices.html, data-status=\"{{ h.connectivity_status|default:h.status|lower }}\" — las fichas source=\"device\" no traen connectivity_status y caen a su status de siempre); status se deja INTACTO porque el Terminal depende de él.\n\n---\n\n### 3. terminal_hosts(org) (función pública)\n\nOrquesta los dos anteriores:\n\npython\ndef terminal_hosts(org):\n devices, taken_ips, racks_by_id = _device_hosts(org)\n profiles = _profile_hosts(org, taken_ips)\n racks = sorted(racks_by_id.values(), key=lambda r: r.name)\n return {\"hosts\": devices + profiles, \"racks\": racks}\n\n\nRetorna:\n- hosts: lista unificada, ordenada (Devices por rack/nombre, luego profiles).\n- racks: lista de racks únicos (para el filtro UI).\n\n---\n\n## Integración con la vista\n\nEn terminal/views.py::index(request):\n\npython\norg = getattr(request.user, \"organization\", None)\nresult = terminal_hosts(org)\nhosts = result[\"hosts\"]\ndevice_racks = result[\"racks\"]\n\ncontext = {\n \"hosts\": hosts,\n \"total_nm_devices\": len(hosts),\n \"device_racks\": device_racks,\n # ...\n}\n\n\nEl template itera {% for host in hosts %} y bifurca según host.source:\n- source == \"device\" → renderiza como equipo de rack con clic directo en TerminalApp.openSession().\n- source == \"profile\" → renderiza como detectado con clic en TerminalApp.quickConnectHost() (modal manual).\n\nLa página Devices (templates/network/devices.html) reutiliza el mismo hosts para su propio filtro de estado, leyendo connectivity_status en vez de status (v1.119.0).\n\n---\n\n## Criterios de exclusión\n\n- Devices: se incluyen si has_network_management == True.\n- Profiles: se incluyen TODOS los de la org (Hub Universal D3 — ya no se filtra por supports_ssh/supports_snmp).\n- Ambos: se excluyen si están en otra organización (RLS) o si el rack está marcado deleted_at.\n- Profiles duplificadas: se excluyen si su IP ya está en un Device de la misma org.\n\n---\n\n## Testing\n\ntests/api/test_terminal_hosts.py cubre:\n\n- ✅ Device en rack aparece con category=\"rack\"\n- ✅ Profile detectado sin asignar aparece con category=\"other\"\n- ✅ Profile con assigned_page wireless/UPS/signage categorizado correctamente\n- ✅ Deduplicación por IP (Device + Profile con la misma IP → una sola fila)\n- ✅ Exclusión de Profile sin SSH/SNMP\n- ✅ Aislamiento de organización (otra org no ve los hosts)\n- ✅ Vista renderiza hosts y dropdown de filtro tipo\n- ✅ (v1.119.0) connectivity_status refleja status_effective del target vinculado; status (conectable SSH) no cambia — test de regresión dedicado.\n\nSuite: 7 tests + regresión v1.119.0, 100% pass (sin regresiones en terminal/racks/network). Sin N+1: linked_monitoring_target ya viene de select_related.\n\n---\n\n## Véase también\n\n- [[entity—terminal—endpoint—index]]\n- [[entity—racks—model—device]]\n- [[entity—network—model—device-profile]]\n- [[entity—network—model—monitoring-target]]\n- [[feature—terminal—todos-dispositivos-filtro-tipo]]\n- [[entity—monitoring—model—monitoring-target]] — documenta status_effective, la fuente de connectivity_status\n”}

Subir