From 91222faffede621ade04c32e38cb6c35c1c25832 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 18 Jul 2026 01:18:14 +0200 Subject: [PATCH] Add Network Design Studio with multi-U Dell rack designer. Includes rack catalog/API, Visio-style drag-and-drop, faceplate assets, readable nameplates, and theme/RDP UI updates so multi-U placement stays correct during design. Co-authored-by: Cursor --- api/main.py | 2997 +++++++++++++++++++++++++++- ui/app.js | 78 +- ui/assets/dell/face-blank-1u.jpg | Bin 0 -> 3692 bytes ui/assets/dell/face-chassis-mx.jpg | Bin 0 -> 23979 bytes ui/assets/dell/face-pdu-1u.jpg | Bin 0 -> 6241 bytes ui/assets/dell/face-server-1u.jpg | Bin 0 -> 28352 bytes ui/assets/dell/face-server-2u.jpg | Bin 0 -> 30554 bytes ui/assets/dell/face-server-4u.jpg | Bin 0 -> 29113 bytes ui/assets/dell/face-storage-2u.jpg | Bin 0 -> 29443 bytes ui/assets/dell/face-switch-1u.jpg | Bin 0 -> 30461 bytes ui/index.html | 113 +- ui/network.js | 1454 ++++++++++++++ ui/ops.js | 126 +- ui/rdp-popout.html | 353 ++++ ui/rdp.js | 53 + ui/reports.js | 1058 +++++++++- ui/styles.css | 2856 +++++++++++++++++++++++++- 17 files changed, 8865 insertions(+), 223 deletions(-) create mode 100644 ui/assets/dell/face-blank-1u.jpg create mode 100644 ui/assets/dell/face-chassis-mx.jpg create mode 100644 ui/assets/dell/face-pdu-1u.jpg create mode 100644 ui/assets/dell/face-server-1u.jpg create mode 100644 ui/assets/dell/face-server-2u.jpg create mode 100644 ui/assets/dell/face-server-4u.jpg create mode 100644 ui/assets/dell/face-storage-2u.jpg create mode 100644 ui/assets/dell/face-switch-1u.jpg create mode 100644 ui/network.js create mode 100644 ui/rdp-popout.html create mode 100644 ui/rdp.js diff --git a/api/main.py b/api/main.py index b5f0040..f1ac8d0 100644 --- a/api/main.py +++ b/api/main.py @@ -1,7 +1,9 @@ import asyncio import json +import re import os +import socket import sqlite3 import ipaddress import logging @@ -34,8 +36,8 @@ class Settings(BaseSettings): vllm_url: str = "http://10.0.10.106:8000/v1" vllm_model: str = "llama3-70b-gptq" vllm_max_model_len: int = 4096 - vllm_max_tokens: int = 700 - chat_system_chars: int = 9000 + vllm_max_tokens: int = 1100 + chat_system_chars: int = 12000 openwebui_email: str = "" openwebui_password: str = "" cockpit_data: str = "/data" @@ -152,6 +154,819 @@ def rows_to_csv(rows: list[dict], columns: list[str] | None = None) -> str: return "\n".join(lines) + "\n" +# --------------------------------------------------------------------------- +# Dell PowerEdge expansion / memory population catalog (from Dell Tech Guides +# + Installation & Service Manuals / KB 000141539). Used for empty-slot maps, +# max capacity, and best-practice guidance — not a live scrape. +# --------------------------------------------------------------------------- + +_DELL_LINKS = { + # Verified live PDFs / product docs (checked 2026-07-17: %PDF or HTTP 200 HTML). + "spec_r740xd": "https://i.dell.com/sites/csdocuments/Product_Docs/en/poweredge-r740xd-spec-sheet.pdf", + "spec_r740": "https://i.dell.com/sites/csdocuments/Product_Docs/en/poweredge-r740-spec-sheet.pdf", + "tech_r740": "https://i.dell.com/sites/csdocuments/Shared-Content_data-Sheets_Documents/en/aa/PowerEdge_R740_R740xd_Technical_Guide.pdf", + "own_r740xd": "https://dl.dell.com/topicspdf/poweredge-r740xd_owners-manual_en-us.pdf", + "docs_r740xd": "https://www.dell.com/support/product-details/en-us/product/poweredge-r740xd/docs", + "docs_r740": "https://www.dell.com/support/product-details/en-us/product/poweredge-r740/docs", + "spec_r640": "https://i.dell.com/sites/csdocuments/Product_Docs/en/poweredge-r640-spec-sheet.pdf", + "own_r640": "https://dl.dell.com/topicspdf/poweredge-r640_owners-manual_en-us.pdf", + "docs_r640": "https://www.dell.com/support/product-details/en-us/product/poweredge-r640/docs", + "spec_r440": "https://i.dell.com/sites/csdocuments/Product_Docs/en/poweredge-r440-spec-sheet.pdf", + "docs_r440": "https://www.dell.com/support/product-details/en-us/product/poweredge-r440/docs", + "spec_r6415": "https://i.dell.com/sites/csdocuments/Shared-Content_data-Sheets_Documents/en/aa/PowerEdge_R6415_Spec_Sheet_EN.pdf", + "ref_r6415": "https://dl.dell.com/topicspdf/poweredge-r6415_reference-guide_en-us.pdf", + "own_r6415": "https://dl.dell.com/topicspdf/poweredge-r6415_owners-manual_en-us.pdf", + "docs_r6415": "https://www.dell.com/support/product-details/en-us/product/poweredge-r6415/docs", + "spec_r7425": "https://i.dell.com/sites/csdocuments/Shared-Content_data-Sheets_Documents/en/aa/PowerEdge-R7425-Spec-Sheet.pdf", + "own_r7425": "https://dl.dell.com/topicspdf/poweredge-r7425_owners-manual_en-us.pdf", + "docs_r7425": "https://www.dell.com/support/product-details/en-us/product/poweredge-r7425/docs", + "spec_r660": "https://www.delltechnologies.com/asset/en-us/products/servers/technical-support/poweredge-r660-spec-sheet.pdf", + "tech_r660": "https://www.delltechnologies.com/asset/en-us/products/servers/technical-support/poweredge-r660-technical-guide.pdf", + "docs_r660": "https://www.dell.com/support/product-details/en-us/product/poweredge-r660/docs", + "spec_xr5610": "https://www.delltechnologies.com/asset/en-us/products/servers/technical-support/poweredge-xr5610-spec-sheet.pdf", + "tech_xr5610": "https://www.delltechnologies.com/asset/en-us/products/servers/technical-support/poweredge-xr5610-technical-guide.pdf", + "docs_xr5610": "https://www.dell.com/support/product-details/en-us/product/poweredge-xr5610/docs", + "support_home": "https://www.dell.com/support/home", +} + + +# 14G dual-socket channel map (R640/R740/R740xd family) +_CH14 = { + "A": {1: 0, 7: 0, 2: 1, 8: 1, 3: 2, 9: 2, 4: 3, 10: 3, 5: 4, 11: 4, 6: 5, 12: 5}, + "B": {1: 0, 7: 0, 2: 1, 8: 1, 3: 2, 9: 2, 4: 3, 10: 3, 5: 4, 11: 4, 6: 5, 12: 5}, +} + + +# Dell datacenter rack catalog — visual U heights + face styles for rack designer +DELL_RACK_CATALOG: list[dict[str, Any]] = [ + # --- PowerEdge 1U --- + {"sku": "PE-R440", "name": "PowerEdge R440", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r440"]}, + {"sku": "PE-R640", "name": "PowerEdge R640", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r640"]}, + {"sku": "PE-R650", "name": "PowerEdge R650", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r650"]}, + {"sku": "PE-R650xs", "name": "PowerEdge R650xs", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r650xs"]}, + {"sku": "PE-R660", "name": "PowerEdge R660", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r660"]}, + {"sku": "PE-R660xs", "name": "PowerEdge R660xs", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r660xs"]}, + {"sku": "PE-R6615", "name": "PowerEdge R6615", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r6615"]}, + {"sku": "PE-R6415", "name": "PowerEdge R6415", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r6415"]}, + {"sku": "PE-R6515", "name": "PowerEdge R6515", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r6515"]}, + {"sku": "PE-R6525", "name": "PowerEdge R6525", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r6525"]}, + {"sku": "PE-R6715", "name": "PowerEdge R6715", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r6715"]}, + {"sku": "PE-R670", "name": "PowerEdge R670", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r670"]}, + {"sku": "PE-XR5610", "name": "PowerEdge XR5610", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["xr5610"]}, + {"sku": "PE-C6520", "name": "PowerEdge C6520", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["c6520"]}, + {"sku": "PE-C6525", "name": "PowerEdge C6525", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["c6525"]}, + {"sku": "PE-C4140", "name": "PowerEdge C4140", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["c4140"]}, + # --- PowerEdge 2U --- + {"sku": "PE-R740", "name": "PowerEdge R740", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r740"]}, + {"sku": "PE-R740xd", "name": "PowerEdge R740xd", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r740xd"]}, + {"sku": "PE-R740xd2", "name": "PowerEdge R740xd2", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r740xd2"]}, + {"sku": "PE-R7425", "name": "PowerEdge R7425", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r7425"]}, + {"sku": "PE-R750", "name": "PowerEdge R750", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r750"]}, + {"sku": "PE-R750xa", "name": "PowerEdge R750xa", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r750xa"]}, + {"sku": "PE-R7515", "name": "PowerEdge R7515", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r7515"]}, + {"sku": "PE-R7525", "name": "PowerEdge R7525", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r7525"]}, + {"sku": "PE-R760", "name": "PowerEdge R760", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r760"]}, + {"sku": "PE-R760xd2", "name": "PowerEdge R760xd2", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r760xd2"]}, + {"sku": "PE-R760xa", "name": "PowerEdge R760xa", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r760xa"]}, + {"sku": "PE-R770", "name": "PowerEdge R770", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r770"]}, + {"sku": "PE-R840", "name": "PowerEdge R840", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r840"]}, + {"sku": "PE-R940", "name": "PowerEdge R940", "family": "PowerEdge", "category": "server", "u_height": 3, "face": "server_4u", "match": ["r940"]}, + {"sku": "PE-R940xa", "name": "PowerEdge R940xa", "family": "PowerEdge", "category": "server", "u_height": 4, "face": "server_4u", "match": ["r940xa"]}, + {"sku": "PE-R340", "name": "PowerEdge R340", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r340"]}, + {"sku": "PE-R450", "name": "PowerEdge R450", "family": "PowerEdge", "category": "server", "u_height": 1, "face": "server_1u", "match": ["r450"]}, + {"sku": "PE-R550", "name": "PowerEdge R550", "family": "PowerEdge", "category": "server", "u_height": 2, "face": "server_2u", "match": ["r550"]}, + {"sku": "PE-T640", "name": "PowerEdge T640", "family": "PowerEdge", "category": "server", "u_height": 5, "face": "server_4u", "match": ["t640"]}, + {"sku": "PE-DSS8440", "name": "DSS8440", "family": "PowerEdge", "category": "server", "u_height": 4, "face": "server_4u", "match": ["dss8440"]}, + {"sku": "PE-XE9680", "name": "PowerEdge XE9680", "family": "PowerEdge", "category": "server", "u_height": 6, "face": "server_4u", "match": ["xe9680"]}, + {"sku": "PE-XE8640", "name": "PowerEdge XE8640", "family": "PowerEdge", "category": "server", "u_height": 4, "face": "server_4u", "match": ["xe8640"]}, + # --- MX / modular --- + {"sku": "PE-MX7000", "name": "PowerEdge MX7000", "family": "PowerEdge MX", "category": "chassis", "u_height": 7, "face": "chassis_mx", "match": ["mx7000"]}, + {"sku": "PE-MX740c", "name": "PowerEdge MX740c", "family": "PowerEdge MX", "category": "blade", "u_height": 0, "face": "blade", "match": ["mx740c"]}, + {"sku": "PE-MX750c", "name": "PowerEdge MX750c", "family": "PowerEdge MX", "category": "blade", "u_height": 0, "face": "blade", "match": ["mx750c"]}, + {"sku": "PE-M640", "name": "PowerEdge M640", "family": "PowerEdge", "category": "blade", "u_height": 0, "face": "blade", "match": ["m640"]}, + # --- Networking / PowerSwitch / Force10 --- + {"sku": "PS-S4048", "name": "PowerSwitch S4048-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["s4048", "force10"]}, + {"sku": "PS-S4148", "name": "PowerSwitch S4148-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["s4148"]}, + {"sku": "PS-S5248", "name": "PowerSwitch S5248F-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["s5248"]}, + {"sku": "PS-S5296", "name": "PowerSwitch S5296F-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["s5296"]}, + {"sku": "PS-N3248", "name": "PowerSwitch N3248TE-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["n3248"]}, + {"sku": "PS-Z9264", "name": "PowerSwitch Z9264F-ON", "family": "PowerSwitch", "category": "switch", "u_height": 2, "face": "switch_2u", "match": ["z9264"]}, + {"sku": "PS-Z9432", "name": "PowerSwitch Z9432F-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["z9432"]}, + {"sku": "PC-5548", "name": "PowerConnect 5548", "family": "PowerConnect", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["powerconnect 5548", "pc5548"]}, + {"sku": "PC-6248", "name": "PowerConnect 6248", "family": "PowerConnect", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["powerconnect 6248", "pc6248"]}, + {"sku": "PC-7048", "name": "PowerConnect 7048", "family": "PowerConnect", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["powerconnect 7048", "pc7048"]}, + {"sku": "PC-8024", "name": "PowerConnect 8024F", "family": "PowerConnect", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["powerconnect 8024", "pc8024"]}, + {"sku": "PC-8132", "name": "PowerConnect 8132F", "family": "PowerConnect", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["powerconnect 8132", "pc8132"]}, + {"sku": "MX-9116n", "name": "MX9116n Fabric Switching Engine", "family": "PowerEdge MX", "category": "switch", "u_height": 0, "face": "fabric", "match": ["mx9116"]}, + {"sku": "MX-5108n", "name": "MX5108n Ethernet Switch", "family": "PowerEdge MX", "category": "switch", "u_height": 0, "face": "fabric", "match": ["mx5108"]}, + # --- Storage --- + {"sku": "ME-4012", "name": "PowerVault ME4012", "family": "PowerVault", "category": "storage", "u_height": 2, "face": "storage_2u", "match": ["me4012"]}, + {"sku": "ME-4024", "name": "PowerVault ME4024", "family": "PowerVault", "category": "storage", "u_height": 2, "face": "storage_2u", "match": ["me4024"]}, + {"sku": "ME-5012", "name": "PowerVault ME5012", "family": "PowerVault", "category": "storage", "u_height": 2, "face": "storage_2u", "match": ["me5012"]}, + {"sku": "ME-5024", "name": "PowerVault ME5024", "family": "PowerVault", "category": "storage", "u_height": 2, "face": "storage_2u", "match": ["me5024"]}, + {"sku": "SC-AllFlash", "name": "SC Series Array", "family": "PowerStore/SC", "category": "storage", "u_height": 3, "face": "storage_3u", "match": ["sc "]}, + {"sku": "PST-5000T", "name": "PowerStore 5000T", "family": "PowerStore/SC", "category": "storage", "u_height": 2, "face": "storage_2u", "match": ["powerstore 5000"]}, + {"sku": "PST-1200T", "name": "PowerStore 1200T", "family": "PowerStore/SC", "category": "storage", "u_height": 2, "face": "storage_2u", "match": ["powerstore 1200"]}, + {"sku": "PST-3200T", "name": "PowerStore 3200T", "family": "PowerStore/SC", "category": "storage", "u_height": 2, "face": "storage_2u", "match": ["powerstore 3200"]}, + {"sku": "DD-6900", "name": "PowerProtect DD6900", "family": "PowerProtect", "category": "storage", "u_height": 4, "face": "storage_3u", "match": ["dd6900"]}, + {"sku": "DD-9400", "name": "PowerProtect DD9400", "family": "PowerProtect", "category": "storage", "u_height": 4, "face": "storage_3u", "match": ["dd9400"]}, + {"sku": "UNITY-380", "name": "Unity XT 380", "family": "Unity", "category": "storage", "u_height": 2, "face": "storage_2u", "match": ["unity"]}, + # --- VxRail appliances --- + {"sku": "VX-E460F", "name": "VxRail E460F", "family": "VxRail", "category": "server", "u_height": 1, "face": "server_1u", "match": ["e460f", "vxrail e"]}, + {"sku": "VX-V470F", "name": "VxRail V470F", "family": "VxRail", "category": "server", "u_height": 1, "face": "server_1u", "match": ["v470f"]}, + {"sku": "VX-P570F", "name": "VxRail P570F", "family": "VxRail", "category": "server", "u_height": 1, "face": "server_1u", "match": ["p570f"]}, + {"sku": "VX-V670F", "name": "VxRail V670F", "family": "VxRail", "category": "server", "u_height": 2, "face": "server_2u", "match": ["v670f"]}, + {"sku": "VX-P670F", "name": "VxRail P670F", "family": "VxRail", "category": "server", "u_height": 2, "face": "server_2u", "match": ["p670f"]}, + {"sku": "VX-S570", "name": "VxRail S570", "family": "VxRail", "category": "server", "u_height": 1, "face": "server_1u", "match": ["s570"]}, + # --- Networking extras --- + {"sku": "PS-S5232", "name": "PowerSwitch S5232F-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["s5232"]}, + {"sku": "PS-S5212", "name": "PowerSwitch S5212F-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["s5212"]}, + {"sku": "PS-N3224", "name": "PowerSwitch N3224T-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["n3224"]}, + {"sku": "PS-Z9100", "name": "PowerSwitch Z9100-ON", "family": "PowerSwitch", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["z9100"]}, + {"sku": "PC-7024", "name": "PowerConnect 7024", "family": "PowerConnect", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["powerconnect 7024"]}, + {"sku": "PC-8100", "name": "PowerConnect 8100", "family": "PowerConnect", "category": "switch", "u_height": 1, "face": "switch_1u", "match": ["powerconnect 8100"]}, + # --- PDU / misc --- + {"sku": "PDU-Geist-0U", "name": "Geist Rack PDU (0U vertical)", "family": "PDU", "category": "pdu", "u_height": 0, "face": "pdu_0u", "match": ["geist", "mg03", "mgs3"]}, + {"sku": "PDU-1U", "name": "Rack PDU 1U horizontal", "family": "PDU", "category": "pdu", "u_height": 1, "face": "pdu_1u", "match": []}, + {"sku": "PDU-APC-0U", "name": "APC Rack PDU 0U", "family": "PDU", "category": "pdu", "u_height": 0, "face": "pdu_0u", "match": ["apc"]}, + {"sku": "PANEL-1U", "name": "Blanking panel 1U", "family": "Accessory", "category": "blank", "u_height": 1, "face": "blank_1u", "match": []}, + {"sku": "PANEL-2U", "name": "Blanking panel 2U", "family": "Accessory", "category": "blank", "u_height": 2, "face": "blank_2u", "match": []}, + {"sku": "CABLE-MGMT-1U", "name": "Cable management arm 1U", "family": "Accessory", "category": "blank", "u_height": 1, "face": "blank_1u", "match": []}, + {"sku": "KVM-1U", "name": "Rack KVM 1U", "family": "Accessory", "category": "custom", "u_height": 1, "face": "custom", "match": ["kvm"]}, + {"sku": "CUSTOM", "name": "Custom device", "family": "Custom", "category": "custom", "u_height": 1, "face": "custom", "match": []}, +] + + +FACE_IMAGE_MAP: dict[str, str] = { + "server_1u": "/assets/dell/face-server-1u.jpg?v=bezel2", + "server_2u": "/assets/dell/face-server-2u.jpg?v=bezel2", + "server_4u": "/assets/dell/face-server-4u.jpg?v=bezel2", + "switch_1u": "/assets/dell/face-switch-1u.jpg?v=bezel2", + "switch_2u": "/assets/dell/face-switch-1u.jpg?v=bezel2", + "storage_2u": "/assets/dell/face-storage-2u.jpg?v=bezel2", + "storage_3u": "/assets/dell/face-storage-2u.jpg?v=bezel2", + "chassis_mx": "/assets/dell/face-chassis-mx.jpg?v=bezel2", + "pdu_1u": "/assets/dell/face-pdu-1u.jpg?v=bezel2", + "pdu_0u": "/assets/dell/face-pdu-1u.jpg?v=bezel2", + "blank_1u": "/assets/dell/face-blank-1u.jpg?v=bezel2", + "blank_2u": "/assets/dell/face-blank-1u.jpg?v=bezel2", + "blade": "/assets/dell/face-chassis-mx.jpg?v=bezel2", + "fabric": "/assets/dell/face-switch-1u.jpg?v=bezel2", + "custom": "/assets/dell/face-server-1u.jpg?v=bezel2", +} + +# Official Dell product shots by SKU (processed to dark background to match faces) +SKU_IMAGE_MAP: dict[str, str] = { + # Prefer shared dark face art for visual consistency across all PowerEdge 1U/2U +} + + +def catalog_image_for(item: dict) -> str: + sku = item.get("sku") or "" + if sku in SKU_IMAGE_MAP: + return SKU_IMAGE_MAP[sku] + face = item.get("face") or "custom" + return FACE_IMAGE_MAP.get(face, FACE_IMAGE_MAP["custom"]) + + +def enrich_catalog_images() -> None: + for item in DELL_RACK_CATALOG: + item["image"] = catalog_image_for(item) + + +enrich_catalog_images() + + +def match_rack_catalog(model: str | None, role: str | None = None) -> dict | None: + m = (model or "").lower() + best = None + for item in DELL_RACK_CATALOG: + for token in item.get("match") or []: + if token and token in m: + best = item + break + if best: + break + if best: + return best + if role == "switch": + return next((x for x in DELL_RACK_CATALOG if x["sku"] == "PS-S4048"), None) + if role == "storage": + return next((x for x in DELL_RACK_CATALOG if x["sku"] == "ME-4024"), None) + if role == "chassis": + return next((x for x in DELL_RACK_CATALOG if x["sku"] == "PE-MX7000"), None) + if role == "pdu": + return next((x for x in DELL_RACK_CATALOG if x["sku"] == "PDU-Geist-0U"), None) + if role == "server": + return next((x for x in DELL_RACK_CATALOG if x["sku"] == "PE-R740"), None) + return None + +MODEL_EXPANSION: dict[str, dict[str, Any]] = { + "PowerEdge R740xd": { + "generation": "14G", + "dimm_slots": 24, + "dimms_per_cpu": 12, + "channels_per_cpu": 6, + "max_memory_tb": 3.0, + "max_memory_note": "Up to 3 TB with LRDIMM (+ DCPMM configs); typical RDIMM max lower per CPU SKU", + "memory_types": ["DDR4 RDIMM", "DDR4 LRDIMM", "NVDIMM-N", "DCPMM/PMem"], + "max_dimm_speed_mts": 2666, + "pcie_slots_max": 8, + "pcie_note": "Up to 8× PCIe 3.0 via riser options; slot count depends on riser kit + CPU count", + "drive_bays_max": 24, + "drive_note": "Chassis-dependent: up to 24× 2.5\" (or mixed 3.5\"/NVMe mid-tray options on xd)", + "cpu_sockets": 2, + "best_practices": [ + "Populate identical DIMMs across all six channels per CPU (6 or 12 DIMMs/CPU) for best bandwidth.", + "White release-tab sockets first, then black (channel rank 1 then rank 2).", + "Do not mix RDIMM and LRDIMM in the same system.", + "Unbalanced channel population reduces memory bandwidth / can lower effective speed.", + "Performance Optimized (4 DIMMs/CPU): slots 1,2,4,5 — (8 DIMMs/CPU): 1,2,4,5,7,8,10,11 (Dell KB 000141539).", + ], + "performance_notes": [ + "Peak memory bandwidth needs one matched DIMM in every channel (6 per CPU) before adding the second rank (slots 7–12).", + "Mixing capacities: place largest DIMMs in the first populated sockets of each channel.", + "DCPMM: up to 6 PMem + 6 DRAM per CPU; full 12-slot population recommended for App Direct / Memory Mode.", + ], + "docs": [ + {"title": "R740xd Spec Sheet (PDF)", "url": _DELL_LINKS["spec_r740xd"], "kind": "spec"}, + {"title": "R740/R740xd Technical Guide (PDF)", "url": _DELL_LINKS["tech_r740"], "kind": "tech"}, + {"title": "R740xd Owner's Manual (PDF)", "url": _DELL_LINKS["own_r740xd"], "kind": "manual"}, + {"title": "Dell Support · R740xd docs", "url": _DELL_LINKS["docs_r740xd"], "kind": "support"}, + ], + "channel_map": _CH14, + "slot_labels": [f"A{i}" for i in range(1, 13)] + [f"B{i}" for i in range(1, 13)], + }, + "PowerEdge R740": { + "generation": "14G", + "dimm_slots": 24, + "dimms_per_cpu": 12, + "channels_per_cpu": 6, + "max_memory_tb": 3.0, + "max_memory_note": "Up to 3 TB with LRDIMM/DCPMM depending on CPU", + "memory_types": ["DDR4 RDIMM", "DDR4 LRDIMM", "NVDIMM-N", "DCPMM/PMem"], + "max_dimm_speed_mts": 2666, + "pcie_slots_max": 8, + "pcie_note": "Up to 8× PCIe 3.0 (riser dependent)", + "drive_bays_max": 16, + "drive_note": "Up to 16× 2.5\" or 8× 3.5\" depending on chassis kit", + "cpu_sockets": 2, + "best_practices": [ + "Same 14G rules as R740xd: balance channels; white tabs before black.", + "Performance Optimized 4/8 DIMM/CPU populations per Dell KB 000141539.", + ], + "performance_notes": [ + "Populate 6 identical DIMMs per CPU before doubling up channels for max throughput.", + ], + "docs": [ + {"title": "R740 Spec Sheet (PDF)", "url": _DELL_LINKS["spec_r740"], "kind": "spec"}, + {"title": "R740/R740xd Technical Guide (PDF)", "url": _DELL_LINKS["tech_r740"], "kind": "tech"}, + {"title": "Dell Support · R740 docs", "url": _DELL_LINKS["docs_r740"], "kind": "support"}, + ], + "channel_map": _CH14, + "slot_labels": [f"A{i}" for i in range(1, 13)] + [f"B{i}" for i in range(1, 13)], + }, + "PowerEdge R640": { + "generation": "14G", + "dimm_slots": 24, + "dimms_per_cpu": 12, + "channels_per_cpu": 6, + "max_memory_tb": 3.0, + "memory_types": ["DDR4 RDIMM", "DDR4 LRDIMM", "DCPMM/PMem"], + "max_dimm_speed_mts": 2666, + "pcie_slots_max": 3, + "pcie_note": "1U: up to 3× PCIe 3.0 (+ rNDC)", + "drive_bays_max": 10, + "drive_note": "Up to 10× 2.5\" (NVMe options vary)", + "cpu_sockets": 2, + "best_practices": [ + "14G dual-socket population: identical DIMMs across 6 channels per CPU.", + "White-tab sockets first; avoid RDIMM+LRDIMM mix.", + ], + "performance_notes": [ + "1U thermal/airflow limits denser GPU/NVMe kits — verify riser/backplane BOM.", + ], + "docs": [ + {"title": "R640 Spec Sheet (PDF)", "url": _DELL_LINKS["spec_r640"], "kind": "spec"}, + {"title": "R640 Owner's Manual (PDF)", "url": _DELL_LINKS["own_r640"], "kind": "manual"}, + {"title": "Dell Support · R640 docs", "url": _DELL_LINKS["docs_r640"], "kind": "support"}, + ], + "channel_map": _CH14, + "slot_labels": [f"A{i}" for i in range(1, 13)] + [f"B{i}" for i in range(1, 13)], + }, + "PowerEdge R440": { + "generation": "14G", + "dimm_slots": 16, + "dimms_per_cpu": 8, + "channels_per_cpu": 6, + "max_memory_tb": 0.512, + "memory_types": ["DDR4 RDIMM"], + "max_dimm_speed_mts": 2666, + "pcie_slots_max": 3, + "drive_bays_max": 10, + "cpu_sockets": 2, + "best_practices": ["Populate channels evenly; match DIMM size/speed/rank per Dell 14G guidance."], + "performance_notes": ["Entry 14G — keep DIMM speeds matched to CPU QPI/UPI memory controller limits."], + "docs": [ + {"title": "R440 Spec Sheet (PDF)", "url": _DELL_LINKS["spec_r440"], "kind": "spec"}, + {"title": "Dell Support · R440 docs", "url": _DELL_LINKS["docs_r440"], "kind": "support"}, + ], + "slot_labels": [f"A{i}" for i in range(1, 9)] + [f"B{i}" for i in range(1, 9)], + }, + "PowerEdge R6415": { + "generation": "14G-AMD", + "dimm_slots": 16, + "dimms_per_cpu": 16, + "channels_per_cpu": 8, + "max_memory_tb": 1.0, + "memory_types": ["DDR4 RDIMM", "DDR4 LRDIMM"], + "max_dimm_speed_mts": 2666, + "pcie_slots_max": 3, + "drive_bays_max": 10, + "cpu_sockets": 1, + "best_practices": [ + "AMD EPYC Naples/Rome 14G: for 4 DIMMs use slots 1,3,5,7 (Dell KB 000141539).", + "Populate evenly across memory channels for Infinity Fabric bandwidth.", + ], + "performance_notes": [ + "EPYC memory performance scales strongly with channels populated — prefer 8 identical DIMMs when possible.", + ], + "docs": [ + {"title": "R6415 Spec Sheet (PDF)", "url": _DELL_LINKS["spec_r6415"], "kind": "spec"}, + {"title": "R6415 Tech Specs / Reference (PDF)", "url": _DELL_LINKS["ref_r6415"], "kind": "tech"}, + {"title": "R6415 Owner's Manual (PDF)", "url": _DELL_LINKS["own_r6415"], "kind": "manual"}, + {"title": "Dell Support · R6415 docs", "url": _DELL_LINKS["docs_r6415"], "kind": "support"}, + ], + "slot_labels": [f"A{i}" for i in range(1, 17)], + }, + "PowerEdge R7425": { + "generation": "14G-AMD", + "dimm_slots": 32, + "dimms_per_cpu": 16, + "channels_per_cpu": 8, + "max_memory_tb": 2.0, + "memory_types": ["DDR4 RDIMM", "DDR4 LRDIMM"], + "max_dimm_speed_mts": 2666, + "pcie_slots_max": 8, + "drive_bays_max": 24, + "cpu_sockets": 2, + "best_practices": [ + "Dual EPYC: mirror CPU1 and CPU2 DIMM populations.", + "4 DIMMs/CPU → slots 1,3,5,7 per Dell AMD 14G guidance.", + ], + "performance_notes": ["Balance both sockets identically to avoid NUMA imbalance."], + "docs": [ + {"title": "R7425 Spec Sheet (PDF)", "url": _DELL_LINKS["spec_r7425"], "kind": "spec"}, + {"title": "R7425 Owner's Manual (PDF)", "url": _DELL_LINKS["own_r7425"], "kind": "manual"}, + {"title": "Dell Support · R7425 docs", "url": _DELL_LINKS["docs_r7425"], "kind": "support"}, + ], + "slot_labels": [f"A{i}" for i in range(1, 17)] + [f"B{i}" for i in range(1, 17)], + }, + "PowerEdge R660": { + "generation": "16G", + "dimm_slots": 32, + "dimms_per_cpu": 16, + "channels_per_cpu": 8, + "max_memory_tb": 8.0, + "memory_types": ["DDR5 RDIMM"], + "max_dimm_speed_mts": 4800, + "pcie_slots_max": 3, + "drive_bays_max": 10, + "cpu_sockets": 2, + "best_practices": [ + "16G DDR5: populate all memory channels evenly; follow white/black tab order in the ISM for the SKU.", + "Use matched DDR5 RDIMM kits; mixing ranks/speeds can downclock the entire domain.", + ], + "performance_notes": [ + "Sapphire Rapids / Emerald Rapids memory bandwidth benefits strongly from full channel population.", + ], + "docs": [ + {"title": "R660 Spec Sheet (PDF)", "url": _DELL_LINKS["spec_r660"], "kind": "spec"}, + {"title": "R660 Technical Guide (PDF)", "url": _DELL_LINKS["tech_r660"], "kind": "tech"}, + {"title": "Dell Support · R660 docs", "url": _DELL_LINKS["docs_r660"], "kind": "support"}, + ], + "slot_labels": [f"A{i}" for i in range(1, 17)] + [f"B{i}" for i in range(1, 17)], + }, + "PowerEdge XR5610": { + "generation": "16G-XR", + "dimm_slots": 8, + "dimms_per_cpu": 8, + "channels_per_cpu": 8, + "max_memory_tb": 1.0, + "max_memory_note": "Dell Spec Sheet: 8× DDR5 RDIMM, up to 1 TB, up to 5600 MT/s (CPU/SKU dependent)", + "memory_types": ["DDR5 RDIMM"], + "max_dimm_speed_mts": 5600, + "pcie_slots_max": 2, + "pcie_note": "Spec Sheet: up to 2× PCIe Gen5 x16 (+ OCP 3.0) — riser/GPU option dependent", + "drive_bays_max": 4, + "drive_note": "Spec Sheet: up to 4× 2.5\" SAS/SATA/NVMe front; BOSS-N1 M.2 is separate boot — not front-bay count", + "cpu_sockets": 1, + "best_practices": [ + "Populate DDR5 channels evenly with matched RDIMMs.", + "Confirm exact DIMM map in the XR5610 Technical Guide / ISM for this BOM.", + ], + "performance_notes": [ + "Single-socket edge platform: memory bandwidth scales with channels populated.", + ], + "docs": [ + {"title": "XR5610 Spec Sheet (PDF)", "url": _DELL_LINKS["spec_xr5610"], "kind": "spec"}, + {"title": "XR5610 Technical Guide (PDF)", "url": _DELL_LINKS["tech_xr5610"], "kind": "tech"}, + {"title": "Dell Support · XR5610 docs", "url": _DELL_LINKS["docs_xr5610"], "kind": "support"}, + ], + "slot_labels": [f"A{i}" for i in range(1, 9)], + }, +} + + +def _normalize_model_key(model: str | None) -> str: + m = (model or "").strip() + if not m: + return "" + if m in MODEL_EXPANSION: + return m + # fuzzy family match + for key in MODEL_EXPANSION: + if key.lower() in m.lower() or m.lower() in key.lower(): + return key + # strip suffixes like "vSAN Ready Node" + for key in MODEL_EXPANSION: + short = key.replace("PowerEdge ", "") + if short.lower() in m.lower(): + return key + return m + + +def _parse_dimm_socket(name: str | None) -> tuple[str | None, int | None]: + import re + + s = name or "" + m = re.search(r"(?:DIMM\.Socket\.|DIMM\s*)([A-D])(\d{1,2})", s, re.I) + if not m: + m = re.search(r"\b([A-D])(\d{1,2})\b", s) + if not m: + return None, None + return m.group(1).upper(), int(m.group(2)) + + +def _disk_size_gb(raw: Any) -> float | None: + """OME serverArrayDisks.Size is already capacity in GB (e.g. '446.62').""" + if raw is None or raw == "": + return None + try: + return round(float(str(raw).strip()), 2) + except Exception: + return None + + +def _mem_size_gb(raw: Any) -> float | None: + """OME serverMemoryDevices.Size is capacity in MB.""" + if raw is None or raw == "": + return None + try: + return round(float(raw) / 1024.0, 1) + except Exception: + return None + + +def build_expansion_report(model: str | None, inventory: dict) -> dict: + """Split live OME facts (current) from Dell catalog max expansion (recommendations).""" + key = _normalize_model_key(model) + catalog_known = bool(MODEL_EXPANSION.get(key)) + spec = dict(MODEL_EXPANSION.get(key) or {}) + if not catalog_known: + spec = { + "generation": None, + "dimm_slots": None, + "max_memory_tb": None, + "pcie_slots_max": None, + "drive_bays_max": None, + "cpu_sockets": None, + "best_practices": [ + "No Cockpit catalog entry for this model — verify DIMM/CPU/drive maxima in the Dell Tech Specs / ISM for the exact Service Tag BOM.", + ], + "performance_notes": [], + "docs": [{"title": "Dell Support home", "url": _DELL_LINKS["support_home"]}], + "slot_labels": [], + "channel_map": {}, + "memory_types": [], + "max_memory_note": "Maximum not asserted — catalog unknown for this model.", + "drive_note": "Bay maximum not asserted — catalog unknown for this model.", + "pcie_note": "PCIe maximum not asserted — catalog unknown for this model.", + } + + mem_raw = inventory.get("serverMemoryDevices") or [] + cpu_raw = inventory.get("serverProcessors") or [] + disk_raw = inventory.get("serverArrayDisks") or [] + cards_raw = inventory.get("serverDeviceCards") or [] + raid_raw = inventory.get("serverRaidControllers") or [] + os_raw = inventory.get("serverOperatingSystems") or [] + + dimm_modules: list[dict] = [] + for dimm in mem_raw: + bank, num = _parse_dimm_socket( + dimm.get("Name") or dimm.get("InstanceId") or dimm.get("DeviceDescription") + ) + if not bank or not num: + continue + label = f"{bank}{num}" + size_gb = _mem_size_gb(dimm.get("Size")) + dimm_modules.append( + { + "slot": label, + "cpu_bank": bank, + "slot_num": num, + "size_gb": size_gb, + "size_mb": dimm.get("Size"), + "rated_mts": dimm.get("Speed"), + "operating_mts": dimm.get("CurrentOperatingSpeed"), + "rank": dimm.get("Rank"), + "type": dimm.get("TypeDetails") or dimm.get("MemoryType") or "DDR", + "manufacturer": dimm.get("Manufacturer"), + "part_number": dimm.get("PartNumber"), + "serial": dimm.get("SerialNumber"), + "status": dimm.get("Status"), + } + ) + dimm_modules.sort(key=lambda s: (s.get("cpu_bank") or "", s.get("slot_num") or 0)) + mem_total_gb = round(sum(s.get("size_gb") or 0 for s in dimm_modules), 1) + rated_speeds = sorted({s["rated_mts"] for s in dimm_modules if s.get("rated_mts") is not None}) + op_speeds = sorted({s["operating_mts"] for s in dimm_modules if s.get("operating_mts") is not None}) + + cpus: list[dict] = [] + for p in cpu_raw: + cpus.append( + { + "slot": p.get("SlotNumber") or p.get("InstanceId"), + "model": p.get("ModelName") or p.get("BrandName"), + "cores": p.get("NumberOfEnabledCores") or p.get("NumberOfCores"), + "threads": p.get("NumberOfEnabledThreads") or p.get("NumberOfThreads"), + "current_mhz": p.get("CurrentSpeed"), + "max_mhz": p.get("MaxSpeed"), + "status": p.get("Status"), + } + ) + cpu_cores_total = sum(int(c.get("cores") or 0) for c in cpus) + + disks: list[dict] = [] + for d in disk_raw: + disks.append( + { + "bay": d.get("SlotNumber"), + "model": d.get("ModelNumber"), + "vendor": d.get("VendorName"), + "media": d.get("MediaType"), + "size_gb": _disk_size_gb(d.get("Size")), + "bus": d.get("BusType"), + "status": d.get("StatusString") or d.get("Status"), + "serial": d.get("SerialNumber"), + "enclosure": d.get("EnclosureId"), + "form_factor": d.get("FormFactor"), + } + ) + disks.sort(key=lambda x: (str(x.get("bus") or ""), x.get("bay") is None, x.get("bay") or 0)) + disk_total_gb = round(sum(d.get("size_gb") or 0 for d in disks), 2) + + adapters: list[dict] = [] + for c in cards_raw: + adapters.append( + { + "slot": str(c.get("SlotNumber") or ""), + "description": c.get("Description"), + "manufacturer": c.get("Manufacturer"), + "slot_type": c.get("SlotType"), + "width": c.get("DatabusWidth"), + } + ) + for r in raid_raw: + adapters.append( + { + "slot": str(r.get("PciSlot") or r.get("Fqdd") or "RAID"), + "description": r.get("Name") or r.get("DeviceDescription"), + "manufacturer": "Dell PERC", + "slot_type": "RAID", + "firmware": r.get("FirmwareVersion"), + "cache_mb": r.get("CacheSizeInMb"), + } + ) + + os_info = [] + for o in os_raw: + os_info.append( + { + "os_name": (o.get("OsName") or "").strip() or None, + "os_version": (o.get("OsVersion") or "").strip() or None, + "hostname": (o.get("Hostname") or "").strip() or None, + } + ) + + current = { + "source": "OME DeviceService inventory (live)", + "memory": { + "installed_gb": mem_total_gb, + "dimm_count": len(dimm_modules), + "modules": dimm_modules, + "rated_mts": rated_speeds, + "operating_mts": op_speeds, + "banks_seen": sorted({m["cpu_bank"] for m in dimm_modules if m.get("cpu_bank")}), + }, + "cpu": { + "sockets_populated": len(cpus), + "cores_total": cpu_cores_total, + "processors": cpus, + }, + "disks": { + "count": len(disks), + "capacity_gb": disk_total_gb, + "items": disks, + }, + "adapters": adapters, + "os": os_info, + } + + populated_map = {m["slot"]: m for m in dimm_modules} + labels = list(spec.get("slot_labels") or []) + slot_map: list[dict] = [] + for label in labels: + bank = label[0] + num = int(label[1:] or 0) + ch_map = (spec.get("channel_map") or {}).get(bank) or {} + if label in populated_map: + row = dict(populated_map[label]) + row["populated"] = True + row["channel"] = ch_map.get(num) + row["rank_color"] = "white" if num <= 6 else "black" + row["speed_mts"] = row.get("operating_mts") or row.get("rated_mts") + slot_map.append(row) + else: + slot_map.append( + { + "slot": label, + "cpu_bank": bank, + "slot_num": num, + "channel": ch_map.get(num), + "rank_color": "white" if num <= 6 else "black", + "populated": False, + } + ) + for label, row in populated_map.items(): + if label not in labels: + extra = dict(row) + extra["populated"] = True + extra["speed_mts"] = extra.get("operating_mts") or extra.get("rated_mts") + slot_map.append(extra) + + empty_n = sum(1 for s in slot_map if not s.get("populated")) if labels else None + dimm_slots_catalog = spec.get("dimm_slots") + max_tb = spec.get("max_memory_tb") + headroom_gb = None + if max_tb is not None: + try: + headroom_gb = round(float(max_tb) * 1024 - mem_total_gb, 1) + except Exception: + headroom_gb = None + + drive_max = spec.get("drive_bays_max") + free_bays = (int(drive_max) - len(disks)) if catalog_known and isinstance(drive_max, int) else None + cpu_max = spec.get("cpu_sockets") + free_cpu = (int(cpu_max) - len(cpus)) if catalog_known and isinstance(cpu_max, int) else None + + findings: list[dict] = [] + if catalog_known and labels: + by_bank: dict[str, list] = {} + for s in slot_map: + by_bank.setdefault(s.get("cpu_bank") or "?", []).append(s) + for bank, bank_slots in by_bank.items(): + ch_counts: dict[int, int] = {} + for s in bank_slots: + if s.get("populated") and s.get("channel") is not None: + ch_counts[int(s["channel"])] = ch_counts.get(int(s["channel"]), 0) + 1 + if ch_counts and spec.get("channels_per_cpu"): + vals = list(ch_counts.values()) + if max(vals) - min(vals) > 0 or len(ch_counts) < int(spec["channels_per_cpu"]): + findings.append( + { + "severity": "warning", + "code": "unbalanced_channels", + "text": f"Bank {bank}: channel population {dict(sorted(ch_counts.items()))} — Dell guidance prefers even channel fill for bandwidth.", + } + ) + if len(op_speeds) > 1: + findings.append( + { + "severity": "warning", + "code": "mixed_operating_speeds", + "text": f"OME reports mixed operating speeds {op_speeds} MT/s — controller usually clocks all DIMMs to the lowest.", + } + ) + if rated_speeds and op_speeds and max(op_speeds) < max(rated_speeds): + findings.append( + { + "severity": "info", + "code": "downclocked", + "text": f"DIMMs rated {rated_speeds} MT/s, operating at {op_speeds} MT/s (CPU/population limited).", + } + ) + + expansion = { + "source": "Dell model catalog in Cockpit (not live measurement)" if catalog_known else "Catalog unknown", + "catalog_known": catalog_known, + "catalog_model": key if catalog_known else None, + "generation": spec.get("generation"), + "memory": { + "max_capacity_tb": max_tb, + "max_capacity_gb": round(float(max_tb) * 1024, 1) if max_tb is not None else None, + "dimm_slots_max": dimm_slots_catalog, + "dimms_installed": len(dimm_modules), + "empty_slots_estimate": empty_n if catalog_known else None, + "headroom_gb": headroom_gb if catalog_known else None, + "supported_types": spec.get("memory_types") or [], + "max_rated_mts": spec.get("max_dimm_speed_mts"), + "note": spec.get("max_memory_note") + or ( + f"Catalog max {max_tb} TB / {dimm_slots_catalog} DIMM slots" + if catalog_known + else "No catalog max — do not treat empty map as measured free slots." + ), + "slot_map": slot_map if catalog_known else [], + }, + "cpu": { + "sockets_max": cpu_max, + "sockets_populated": len(cpus), + "sockets_free_estimate": free_cpu, + "note": ( + f"Chassis supports up to {cpu_max} CPU socket(s); OME sees {len(cpus)} populated." + if catalog_known and cpu_max is not None + else "CPU socket maximum not in Cockpit catalog for this model." + ), + }, + "disks": { + "bays_max": drive_max if catalog_known else None, + "drives_installed": len(disks), + "bays_free_estimate": free_bays, + "note": spec.get("drive_note") + or ( + "Bay maximum from Dell chassis options — verify BOM; M.2/BOSS may not count as front bays." + if catalog_known + else "Drive bay maximum unknown for this model." + ), + }, + "pcie": { + "slots_max": spec.get("pcie_slots_max") if catalog_known else None, + "note": spec.get("pcie_note") + or ("PCIe max depends on riser kit." if catalog_known else "PCIe maximum unknown for this model."), + }, + "best_practices": spec.get("best_practices") or [], + "performance_notes": spec.get("performance_notes") or [], + "docs": spec.get("docs") or [], + } + + return { + "model": model, + "catalog_key": key if catalog_known else None, + "generic": not catalog_known, + "current": current, + "expansion": expansion, + "findings": findings, + "spec": { + "generation": expansion.get("generation"), + "dimm_slots": expansion["memory"].get("dimm_slots_max"), + "max_memory_tb": expansion["memory"].get("max_capacity_tb"), + "max_memory_note": expansion["memory"].get("note"), + "memory_types": expansion["memory"].get("supported_types"), + "max_dimm_speed_mts": expansion["memory"].get("max_rated_mts"), + "pcie_slots_max": expansion["pcie"].get("slots_max"), + "pcie_note": expansion["pcie"].get("note"), + "drive_bays_max": expansion["disks"].get("bays_max"), + "drive_note": expansion["disks"].get("note"), + "cpu_sockets": expansion["cpu"].get("sockets_max"), + "best_practices": expansion.get("best_practices") or [], + "performance_notes": expansion.get("performance_notes") or [], + "docs": expansion.get("docs") or [], + }, + "memory": { + "total_gb": mem_total_gb, + "populated": len(dimm_modules), + "empty": empty_n, + "headroom_gb": headroom_gb, + "slots": slot_map if slot_map else [{**m, "populated": True, "speed_mts": m.get("operating_mts") or m.get("rated_mts")} for m in dimm_modules], + "banks": current["memory"]["banks_seen"], + }, + "processors": cpus, + "disks": { + "installed": len(disks), + "max_bays": drive_max if catalog_known else None, + "free_bays_estimate": free_bays, + "items": [{**d, "slot": d.get("bay"), "name": d.get("model")} for d in disks], + }, + "pcie": { + "max_slots": expansion["pcie"].get("slots_max"), + "items": adapters, + "note": expansion["pcie"].get("note"), + }, + } + + + def subnet_of(ip: str | None) -> str: if not ip or ip.count(".") != 3: return "unknown" @@ -170,6 +985,83 @@ def pick_ip(device: dict) -> str | None: return None +def pick_management(device: dict) -> dict: + """Extract iDRAC/management IP plus OS hostname hints from OME DeviceManagement.""" + mgmt = (device.get("DeviceManagement") or [{}])[0] or {} + idrac_ip = None + for m in device.get("DeviceManagement") or []: + addr = m.get("NetworkAddress") + if addr and str(addr).count(".") == 3: + idrac_ip = addr + mgmt = m + break + os_hostname = (mgmt.get("InstrumentationName") or "").strip() or None + dns_name = (mgmt.get("DnsName") or "").strip() or None + return { + "idrac_ip": idrac_ip, + "os_hostname": os_hostname, + "mgmt_dns_name": dns_name, + "mac": mgmt.get("MacAddress"), + } + + +async def resolve_host_ips(hostname: str | None) -> list[str]: + if not hostname or hostname.count(".") == 3: + return [] + loop = asyncio.get_running_loop() + try: + infos = await loop.getaddrinfo(hostname, None, family=socket.AF_INET) + return sorted({i[4][0] for i in infos}) + except Exception: + return [] + + +async def enrich_rdp_targets(devices: list[dict]) -> None: + """DNS-resolve OS hostnames from OME InstrumentationName → RDP candidate IPs.""" + hostnames: set[str] = set() + for n in devices: + h = (n.get("os_hostname") or "").strip() + if h and h.count(".") != 3: + hostnames.add(h) + # also try short name + if "." in h: + hostnames.add(h.split(".", 1)[0]) + + resolved: dict[str, list[str]] = {} + sem = asyncio.Semaphore(20) + + async def one(h: str): + async with sem: + resolved[h] = await resolve_host_ips(h) + + if hostnames: + await asyncio.gather(*[one(h) for h in hostnames]) + + for n in devices: + idrac = n.get("idrac_ip") or n.get("ip") + h = (n.get("os_hostname") or "").strip() + ips: list[str] = [] + seen: set[str] = set() + for candidate in (h, h.split(".", 1)[0] if h and "." in h else None): + if not candidate: + continue + for ip in resolved.get(candidate) or []: + if ip not in seen: + seen.add(ip) + ips.append(ip) + # Prefer OS IPs that differ from iDRAC + os_ips = [ip for ip in ips if ip != idrac] + rdp_ips = os_ips or ips + n["rdp_ips"] = rdp_ips + n["rdp_host"] = rdp_ips[0] if rdp_ips else None + n["os_ips"] = os_ips + # Windows hint from name / later OS inventory + blob = f"{n.get('name') or ''} {n.get('os_hostname') or ''} {n.get('os_name') or ''}".lower() + n["is_windows"] = any(k in blob for k in ("windows", "hyperv", "hyper-v", "-hv", "hv0")) or bool( + n.get("os_name") and "windows" in str(n.get("os_name")).lower() + ) + + async def fetch_power(client: httpx.AsyncClient, base: str, headers: dict, device_id: int) -> dict: try: r = await client.get(f"{base}/api/DeviceService/Devices({device_id})/Power", headers=headers) @@ -270,26 +1162,52 @@ async def ome_fetch() -> dict: subnet_map: dict[str, list] = defaultdict(list) model_counts: dict[str, int] = defaultdict(int) idrac_count = server_count = connected = powered = 0 + switch_count = chassis_count = pdu_count = storage_count = 0 total_watts = 0.0 watts_samples = 0 status_counts: dict[str, int] = defaultdict(int) for d in devices_raw: - ip = pick_ip(d) + mgmt = pick_management(d) + ip = mgmt.get("idrac_ip") or pick_ip(d) subnet = subnet_of(ip) dtype = d.get("Type") sub = d.get("SubDeviceType") or "" + model = d.get("Model") or "Unknown" is_server = dtype == 1000 is_idrac = sub == "iDRAC" + is_switch = dtype == 7000 or "switch" in (sub or "").lower() or "switch" in model.lower() + is_chassis = dtype == 2000 or "chassis" in (sub or "").lower() + is_pdu = dtype == 100 + is_storage = dtype == 5000 or "storage" in (sub or "").lower() + if is_switch: + role = "switch" + elif is_chassis: + role = "chassis" + elif is_pdu: + role = "pdu" + elif is_storage: + role = "storage" + elif is_server: + role = "server" + else: + role = "other" conn = bool(d.get("ConnectionState")) power_state = d.get("PowerState") status = str(d.get("Status") or "") status_counts[status] += 1 - model = d.get("Model") or "Unknown" if is_server: model_counts[model] += 1 if is_idrac: idrac_count += 1 + if is_switch: + switch_count += 1 + if is_chassis: + chassis_count += 1 + if is_pdu: + pdu_count += 1 + if is_storage: + storage_count += 1 if is_server: server_count += 1 if conn: @@ -317,9 +1235,22 @@ async def ome_fetch() -> dict: "powered_on": power_state == 17, "status": status, "ip": ip, + "idrac_ip": mgmt.get("idrac_ip") or ip, + "os_hostname": mgmt.get("os_hostname"), + "mgmt_dns_name": mgmt.get("mgmt_dns_name"), + "rdp_host": None, + "rdp_ips": [], + "os_ips": [], + "is_windows": False, "subnet": subnet, + "role": role, "is_server": is_server, "is_idrac": is_idrac, + "is_switch": is_switch, + "is_chassis": is_chassis, + "is_pdu": is_pdu, + "is_storage": is_storage, + "chassis_service_tag": d.get("ChassisServiceTag"), "idrac_url": f"https://{ip}" if ip else None, "watts": watts, "avg_watts": pwr.get("avg_watts"), @@ -331,16 +1262,24 @@ async def ome_fetch() -> dict: devices.append(node) subnet_map[subnet].append(node["id"]) + await enrich_rdp_targets(devices) + subnets = [] for cidr, ids in sorted(subnet_map.items(), key=lambda x: -len(x[1])): members = [n for n in devices if n["id"] in ids] sub_watts = sum(n["watts"] or 0 for n in members if n.get("watts") is not None) + switches = [n for n in members if n.get("is_switch")] subnets.append({ "cidr": cidr, "count": len(members), "connected": sum(1 for n in members if n["connected"]), "watts": round(sub_watts, 1) if sub_watts else None, "device_ids": ids, + "has_switch": bool(switches), + "switches": [ + {"id": s["id"], "name": s["name"], "service_tag": s.get("service_tag"), "ip": s.get("ip")} + for s in switches + ], }) groups = [ @@ -362,11 +1301,19 @@ async def ome_fetch() -> dict: current_ids: set[int] = set() def _role(n: dict) -> str: + if n.get("is_switch"): + return "switch" + if n.get("is_chassis"): + return "chassis" if n.get("is_idrac"): return "iDRAC" if n.get("is_server"): return "server" - return "device" + if n.get("is_storage"): + return "storage" + if n.get("is_pdu"): + return "PDU" + return n.get("role") or "device" for n in devices: did = n.get("id") @@ -535,6 +1482,10 @@ async def ome_fetch() -> dict: "total": len(devices), "idracs": idrac_count, "servers": server_count, + "switches": switch_count, + "chassis": chassis_count, + "pdus": pdu_count, + "storage": storage_count, "connected": connected, "powered_on": powered, "offline": max(0, server_count - connected), @@ -569,6 +1520,509 @@ async def broadcast(payload: dict): CLIENTS.discard(ws) + +def _seed_atc_racks() -> None: + """Idempotent seed: ATC1 (8×42U) + ATC2 (6×42U).""" + now = time.time() + with _db() as conn: + n = conn.execute("SELECT COUNT(*) AS c FROM racks").fetchone()["c"] + if n > 0: + return + conn.execute( + "INSERT OR IGNORE INTO sites(id, name, sort_order) VALUES (?, ?, ?)", + ("ATC1", "ATC1", 1), + ) + conn.execute( + "INSERT OR IGNORE INTO sites(id, name, sort_order) VALUES (?, ?, ?)", + ("ATC2", "ATC2", 2), + ) + for i in range(1, 9): + conn.execute( + """ + INSERT INTO racks(site_id, name, units, sort_order, created_at, updated_at) + VALUES (?, ?, 42, ?, ?, ?) + """, + ("ATC1", f"ATC1-R{i:02d}", i, now, now), + ) + for i in range(1, 7): + conn.execute( + """ + INSERT INTO racks(site_id, name, units, sort_order, created_at, updated_at) + VALUES (?, ?, 42, ?, ?, ?) + """, + ("ATC2", f"ATC2-R{i:02d}", i, now, now), + ) + log.info("Seeded ATC racks: ATC1×8 + ATC2×6 (42U)") + + +def build_network_fabric(devices: list[dict] | None = None) -> dict: + """Inferred switch↔server / chassis↔blade edges (no LLDP).""" + devices = list(devices if devices is not None else (STATE.get("devices") or [])) + by_id = {d.get("id"): d for d in devices if d.get("id") is not None} + edges: list[dict] = [] + seen: set[tuple] = set() + + def add_edge(a: int, b: int, kind: str): + if a is None or b is None or a == b: + return + key = (min(a, b), max(a, b), kind) + if key in seen: + return + seen.add(key) + da, db = by_id.get(a) or {}, by_id.get(b) or {} + edges.append( + { + "from_id": a, + "to_id": b, + "kind": kind, + "confidence": "inferred", + "note": ( + "management subnet" + if kind == "subnet" + else "MX chassis / blade" + ), + "live": bool(da.get("connected") and db.get("connected")), + } + ) + + switches = [d for d in devices if d.get("is_switch")] + for sw in switches: + cidr = sw.get("subnet") or "unknown" + if cidr == "unknown": + continue + for d in devices: + if d.get("id") == sw.get("id"): + continue + if d.get("subnet") != cidr: + continue + if d.get("is_switch") or d.get("is_pdu"): + continue + # Prefer linking servers/storage/chassis endpoints + if d.get("is_server") or d.get("is_storage") or d.get("is_chassis") or d.get("is_idrac"): + add_edge(sw["id"], d["id"], "subnet") + + chassis_list = [d for d in devices if d.get("is_chassis")] + for ch in chassis_list: + cst = (ch.get("service_tag") or "").upper() + for d in devices: + if not d.get("is_server"): + continue + model = (d.get("model") or "").upper() + blade_cst = (d.get("chassis_service_tag") or "").upper() + if cst and blade_cst and blade_cst == cst: + add_edge(ch["id"], d["id"], "chassis") + elif "MX" in model and "MX7000" not in model: + # fallback: all MX blades to MX chassis in same environment + add_edge(ch["id"], d["id"], "chassis") + + return { + "edges": edges, + "note": "Edges inferred from management subnet / MX chassis — not LLDP.", + "switches": [ + { + "id": s.get("id"), + "name": s.get("name"), + "service_tag": s.get("service_tag"), + "ip": s.get("ip") or s.get("idrac_ip"), + "subnet": s.get("subnet"), + "connected": s.get("connected"), + "model": s.get("model"), + "connected_device_ids": [ + e["to_id"] if e["from_id"] == s.get("id") else e["from_id"] + for e in edges + if e["kind"] == "subnet" + and (e["from_id"] == s.get("id") or e["to_id"] == s.get("id")) + ], + } + for s in switches + ], + "summary": { + "devices": len(devices), + "edges": len(edges), + "switches": len(switches), + "chassis": sum(1 for d in devices if d.get("is_chassis")), + }, + } + + +def _default_u_height(device: dict | None) -> int: + if not device: + return 1 + cat = match_rack_catalog(device.get("model"), device.get("role")) + if cat is not None: + uh = int(cat.get("u_height") or 0) + return uh if uh > 0 else 1 # 0U vertical PDUs place as 1U marker unless side=left/right + if device.get("is_chassis"): + return 7 + if device.get("is_storage"): + return 2 + if device.get("is_switch"): + return 1 + return 1 + + +def _rack_overlap( + conn: sqlite3.Connection, + rack_id: int, + u_start: int, + u_height: int, + side: str = "front", + exclude_item_id: int | None = None, + exclude_device_id: int | None = None, +) -> bool: + if side in ("left", "right"): + return False # side PDUs do not consume front/rear U map + u_end = u_start + max(u_height, 1) - 1 + rows = conn.execute( + "SELECT id, device_id, u_start, u_height, side FROM rack_items WHERE rack_id=?", + (rack_id,), + ).fetchall() + for r in rows: + if exclude_item_id is not None and r["id"] == exclude_item_id: + continue + if exclude_device_id is not None and r["device_id"] == exclude_device_id: + continue + rside = (r["side"] or "front") + if rside in ("left", "right"): + continue + if rside != side: + continue + a, b = r["u_start"], r["u_start"] + r["u_height"] - 1 + if not (u_end < a or u_start > b): + return True + return False + + +def _enrich_rack_item(item: dict, devices: dict) -> dict: + out = dict(item) + dev = devices.get(item.get("device_id")) if item.get("device_id") is not None else None + sku = item.get("catalog_sku") + cat = None + if sku: + cat = next((c for c in DELL_RACK_CATALOG if c["sku"] == sku), None) + if not cat and dev: + cat = match_rack_catalog(dev.get("model"), dev.get("role")) + if cat: + out["catalog"] = {k: cat[k] for k in ("sku", "name", "family", "category", "u_height", "face", "image") if k in cat} + out["face"] = cat.get("face") + out["image"] = cat.get("image") or catalog_image_for(cat) + out["category"] = cat.get("category") + else: + out["catalog"] = None + out["face"] = "custom" + out["image"] = FACE_IMAGE_MAP["custom"] + out["category"] = (dev or {}).get("role") or "custom" + if dev: + out["device"] = { + "id": dev.get("id"), + "name": dev.get("name"), + "model": dev.get("model"), + "service_tag": dev.get("service_tag"), + "role": dev.get("role"), + "ip": dev.get("ip") or dev.get("idrac_ip"), + "connected": dev.get("connected"), + "is_switch": dev.get("is_switch"), + "is_server": dev.get("is_server"), + "is_storage": dev.get("is_storage"), + "is_chassis": dev.get("is_chassis"), + "is_pdu": dev.get("is_pdu"), + } + out["label"] = item.get("label") or dev.get("name") + out["model"] = dev.get("model") + else: + out["device"] = None + out["label"] = item.get("label") or ((cat or {}).get("name") if cat else "Custom") + out["model"] = (cat or {}).get("name") + return out + + +def _racks_payload() -> dict: + _migrate_rack_items() + devices = {d.get("id"): d for d in (STATE.get("devices") or []) if d.get("id") is not None} + with _db() as conn: + sites = [dict(r) for r in conn.execute("SELECT * FROM sites ORDER BY sort_order").fetchall()] + racks = [dict(r) for r in conn.execute("SELECT * FROM racks ORDER BY site_id, sort_order").fetchall()] + items = [dict(r) for r in conn.execute("SELECT * FROM rack_items ORDER BY u_start DESC").fetchall()] + placed_ids = {i["device_id"] for i in items if i.get("device_id") is not None} + by_site: dict[str, list] = {} + for rk in racks: + card = dict(rk) + card["items"] = [] + card["placements"] = [] # backward compat alias + for it in items: + if it["rack_id"] != rk["id"]: + continue + enriched = _enrich_rack_item(it, devices) + card["items"].append(enriched) + # compat shape for older UI bits + card["placements"].append( + { + "device_id": it.get("device_id"), + "rack_id": it["rack_id"], + "u_start": it["u_start"], + "u_height": it["u_height"], + "item_id": it["id"], + "side": it.get("side") or "front", + "face": enriched.get("face"), + "catalog": enriched.get("catalog"), + "label": enriched.get("label"), + "device": enriched.get("device") or { + "id": it.get("device_id"), + "name": enriched.get("label"), + "model": enriched.get("model"), + "role": enriched.get("category"), + "service_tag": None, + }, + } + ) + by_site.setdefault(rk["site_id"], []).append(card) + site_out = [{**s, "racks": by_site.get(s["id"], [])} for s in sites] + unplaced = [] + for d in STATE.get("devices") or []: + if d.get("id") in placed_ids: + continue + if not (d.get("is_server") or d.get("is_switch") or d.get("is_chassis") or d.get("is_storage") or d.get("is_pdu")): + continue + cat = match_rack_catalog(d.get("model"), d.get("role")) + unplaced.append( + { + "id": d.get("id"), + "name": d.get("name"), + "model": d.get("model"), + "service_tag": d.get("service_tag"), + "role": d.get("role"), + "ip": d.get("ip") or d.get("idrac_ip"), + "connected": d.get("connected"), + "default_u_height": _default_u_height(d), + "catalog_sku": (cat or {}).get("sku"), + "face": (cat or {}).get("face") or "custom", + "image": (cat or {}).get("image") or catalog_image_for(cat or {"face": "custom"}), + "is_switch": d.get("is_switch"), + "is_server": d.get("is_server"), + "is_storage": d.get("is_storage"), + "is_chassis": d.get("is_chassis"), + "is_pdu": d.get("is_pdu"), + } + ) + unplaced.sort(key=lambda x: ((x.get("role") or ""), (x.get("name") or "").lower())) + return { + "sites": site_out, + "items": items, + "placements": items, + "unplaced": unplaced, + "catalog": DELL_RACK_CATALOG, + } + + +def _switch_port_count(device: dict | None) -> int: + """Default front-panel port count from model (OME has no LLDP ports here).""" + model = ((device or {}).get("model") or "").lower() + if "s4048" in model: + return 48 + if "s5248" in model or "s5296" in model: + return 48 + if "s4148" in model: + return 48 + if "mx9116" in model: + return 16 + if "mx5108" in model: + return 8 + return 48 + + +def _port_label(port_num: int, total: int = 48) -> str: + return f"Eth1/{port_num}" + + +def build_switch_portmap(switch_id: int) -> dict: + devices = {d.get("id"): d for d in (STATE.get("devices") or []) if d.get("id") is not None} + sw = devices.get(switch_id) + if not sw or not sw.get("is_switch"): + # still allow if type 7000 missing flag somehow + sw = next((d for d in (STATE.get("devices") or []) if d.get("id") == switch_id), None) + if not sw: + raise HTTPException(404, "Switch not found in fleet") + nports = _switch_port_count(sw) + with _db() as conn: + rows = conn.execute( + "SELECT * FROM port_links WHERE switch_id=? ORDER BY switch_port", + (switch_id,), + ).fetchall() + links = [dict(r) for r in rows] + by_port = {int(l["switch_port"]): l for l in links} + ports = [] + for i in range(1, nports + 1): + link = by_port.get(i) + peer = devices.get(link["device_id"]) if link else None + ports.append( + { + "port": i, + "label": _port_label(i, nports), + "wired": bool(link), + "link": ( + { + "id": link["id"], + "device_id": link["device_id"], + "device_port": link.get("device_port") or "", + "note": link.get("note") or "", + "device": { + "id": peer.get("id"), + "name": peer.get("name"), + "service_tag": peer.get("service_tag"), + "ip": peer.get("ip") or peer.get("idrac_ip"), + "model": peer.get("model"), + "role": peer.get("role"), + "connected": peer.get("connected"), + } + if peer + else None, + } + if link + else None + ), + } + ) + return { + "switch": { + "id": sw.get("id"), + "name": sw.get("name"), + "model": sw.get("model"), + "service_tag": sw.get("service_tag"), + "ip": sw.get("ip") or sw.get("idrac_ip"), + "subnet": sw.get("subnet"), + "connected": sw.get("connected"), + "port_count": nports, + }, + "ports": ports, + "wired_count": sum(1 for p in ports if p["wired"]), + "note": "Port wiring is manual (OME has no LLDP port map for this switch). Map switch port → server NIC/port.", + } + + +def _seed_atc_vlans() -> None: + """Seed known ATC lab VLANs/subnets (editable).""" + now = time.time() + defaults = [ + (40, "iDRAC / OOB A", "10.0.40.0/24", "Out-of-band management (has S4048)", "ATC1", "#00a8e8", 1), + (41, "iDRAC / OOB B", "10.0.41.0/24", "Out-of-band management", "ATC1", "#3dffe0", 2), + (42, "iDRAC / OOB C", "10.0.42.0/24", "Out-of-band management", "ATC2", "#7c5cff", 3), + (10, "Production / OS", "10.0.10.0/24", "Hypervisor & Windows OS / RDP", "ATC1", "#ff9a3c", 4), + (11, "Lab / OS", "10.0.11.0/24", "Lab OS network", "ATC2", "#ff5c5c", 5), + (120, "Secondary OS", "10.0.120.0/24", "Secondary OS / RDP path", "ATC1", "#f0d060", 6), + ] + with _db() as conn: + n = conn.execute("SELECT COUNT(*) AS c FROM vlans").fetchone()["c"] + if n > 0: + return + for vlan_id, name, cidr, purpose, site, color, so in defaults: + conn.execute( + """ + INSERT INTO vlans(vlan_id, name, cidr, purpose, site_id, color, sort_order, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + (vlan_id, name, cidr, purpose, site, color, so, now, now), + ) + log.info("Seeded ATC VLAN catalog (%s entries)", len(defaults)) + + +def build_vlan_map() -> dict: + """VLANs from catalog + live membership from fleet IPs (mgmt + RDP).""" + _seed_atc_vlans() + _migrate_rack_items() + devices = STATE.get("devices") or [] + with _db() as conn: + rows = [dict(r) for r in conn.execute("SELECT * FROM vlans ORDER BY sort_order, vlan_id").fetchall()] + + def ips_for(d: dict) -> list[str]: + out = [] + for k in ("ip", "idrac_ip"): + v = d.get(k) + if v and v not in out: + out.append(v) + for v in d.get("rdp_ips") or []: + if v and v not in out: + out.append(v) + return out + + def in_cidr(ip: str, cidr: str) -> bool: + try: + return ipaddress.ip_address(ip) in ipaddress.ip_network(cidr, strict=False) + except Exception: + return False + + vlans = [] + for v in rows: + members = [] + for d in devices: + matched = [ip for ip in ips_for(d) if in_cidr(ip, v["cidr"])] + if not matched: + continue + members.append( + { + "id": d.get("id"), + "name": d.get("name"), + "service_tag": d.get("service_tag"), + "role": d.get("role"), + "connected": d.get("connected"), + "ips": matched, + "model": d.get("model"), + } + ) + members.sort(key=lambda m: (m.get("name") or "").lower()) + vlans.append( + { + **v, + "member_count": len(members), + "connected_count": sum(1 for m in members if m.get("connected")), + "members": members[:80], + } + ) + + # Also surface discovered /24s not in catalog + seen = {v["cidr"] for v in rows} + discovered = [] + buckets: dict[str, int] = {} + for d in devices: + for ip in ips_for(d): + if ip.count(".") != 3: + continue + cidr = ".".join(ip.split(".")[:3]) + ".0/24" + if cidr.startswith("127."): + continue + buckets[cidr] = buckets.get(cidr, 0) + 1 + for cidr, cnt in sorted(buckets.items(), key=lambda x: -x[1]): + if cidr not in seen: + discovered.append({"cidr": cidr, "device_ips": cnt, "in_catalog": False}) + + return { + "vlans": vlans, + "discovered_subnets": discovered, + "note": "VLAN names are an ATC catalog (editable). Membership is live from OME mgmt/RDP IPs.", + } + + +def _migrate_rack_items() -> None: + """Copy legacy rack_placements into flexible rack_items once.""" + with _db() as conn: + n = conn.execute("SELECT COUNT(*) AS c FROM rack_items").fetchone()["c"] + if n > 0: + return + try: + rows = conn.execute("SELECT * FROM rack_placements").fetchall() + except Exception: + return + now = time.time() + for r in rows: + conn.execute( + """ + INSERT INTO rack_items(rack_id, device_id, catalog_sku, label, u_start, u_height, side, notes, updated_at) + VALUES (?, ?, NULL, NULL, ?, ?, 'front', NULL, ?) + """, + (r["rack_id"], r["device_id"], r["u_start"], r["u_height"], now), + ) + if rows: + log.info("Migrated %s rack_placements → rack_items", len(rows)) + ADMINS = [ {"id": "jody", "name": "Jody van Dongen", "role": "ATC Datacenter Admin", "email": "jody.van.dongen@dell.com"}, {"id": "laurens", "name": "Laurens Rammers", "role": "ATC Datacenter Admin", "email": "laurens.rammers@dell.com"}, @@ -586,7 +2040,66 @@ def _db(): return conn +def _backup_ops_db(reason: str = "manual") -> Path | None: + """Snapshot ops.db so rebuilds / wipes do not silently lose ticket history.""" + try: + if not DB_PATH.exists() or DB_PATH.stat().st_size < 100: + return None + bak_dir = DATA_DIR / "backups" + bak_dir.mkdir(parents=True, exist_ok=True) + stamp = time.strftime("%Y%m%d-%H%M%S") + dest = bak_dir / f"ops-{stamp}-{reason}-{int(time.time()*1000)%100000}.db" + import shutil + shutil.copy2(DB_PATH, dest) + # keep last 20 backups + olds = sorted(bak_dir.glob("ops-*.db"), key=lambda x: x.stat().st_mtime, reverse=True) + for stale in olds[20:]: + try: + stale.unlink() + except Exception: + pass + return dest + except Exception as e: + log.warning("ops.db backup failed: %s", e) + return None + + +def _restore_ops_db_if_empty() -> bool: + """If tickets table is empty but a backup exists, restore the newest backup with tickets.""" + try: + with _db() as conn: + n = conn.execute("SELECT COUNT(*) AS c FROM tickets").fetchone()["c"] + if n > 0: + return False + bak_dir = DATA_DIR / "backups" + if not bak_dir.exists(): + return False + candidates = sorted(bak_dir.glob("ops-*.db"), key=lambda x: x.stat().st_mtime, reverse=True) + for cand in candidates: + try: + c2 = sqlite3.connect(cand) + c2.row_factory = sqlite3.Row + cnt = c2.execute("SELECT COUNT(*) AS c FROM tickets").fetchone()["c"] + c2.close() + if cnt <= 0: + continue + import shutil + # preserve empty current as .empty + if DB_PATH.exists(): + shutil.copy2(DB_PATH, bak_dir / f"ops-empty-before-restore-{int(time.time())}.db") + shutil.copy2(cand, DB_PATH) + log.warning("Restored ops.db from backup %s (%s tickets)", cand.name, cnt) + return True + except Exception as e: + log.debug("skip backup %s: %s", cand, e) + return False + except Exception as e: + log.warning("ops.db restore check failed: %s", e) + return False + + def init_db(): + DATA_DIR.mkdir(parents=True, exist_ok=True) with _db() as conn: conn.executescript( """ @@ -614,6 +2127,75 @@ def init_db(): cols = {r[1] for r in conn.execute("PRAGMA table_info(tickets)").fetchall()} if "accepted_by" not in cols: conn.execute("ALTER TABLE tickets ADD COLUMN accepted_by TEXT") + conn.executescript( + """ + CREATE TABLE IF NOT EXISTS sites ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + sort_order INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS racks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + site_id TEXT NOT NULL REFERENCES sites(id), + name TEXT NOT NULL, + units INTEGER NOT NULL DEFAULT 42, + sort_order INTEGER NOT NULL, + created_at REAL NOT NULL, + updated_at REAL NOT NULL + ); + CREATE TABLE IF NOT EXISTS rack_placements ( + device_id INTEGER PRIMARY KEY, + rack_id INTEGER NOT NULL REFERENCES racks(id) ON DELETE CASCADE, + u_start INTEGER NOT NULL, + u_height INTEGER NOT NULL, + updated_at REAL NOT NULL + ); + CREATE TABLE IF NOT EXISTS rack_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + rack_id INTEGER NOT NULL REFERENCES racks(id) ON DELETE CASCADE, + device_id INTEGER, + catalog_sku TEXT, + label TEXT, + u_start INTEGER NOT NULL, + u_height INTEGER NOT NULL, + side TEXT NOT NULL DEFAULT 'front', + notes TEXT, + updated_at REAL NOT NULL + ); + CREATE TABLE IF NOT EXISTS port_links ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + switch_id INTEGER NOT NULL, + switch_port INTEGER NOT NULL, + device_id INTEGER NOT NULL, + device_port TEXT, + note TEXT, + updated_at REAL NOT NULL, + UNIQUE(switch_id, switch_port) + ); + CREATE TABLE IF NOT EXISTS vlans ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + vlan_id INTEGER, + name TEXT NOT NULL, + cidr TEXT NOT NULL, + purpose TEXT, + site_id TEXT, + color TEXT, + sort_order INTEGER NOT NULL DEFAULT 0, + created_at REAL NOT NULL, + updated_at REAL NOT NULL + ); + """ + ) + _restore_ops_db_if_empty() + _seed_atc_racks() + _seed_atc_vlans() + n = 0 + try: + with _db() as conn: + n = conn.execute("SELECT COUNT(*) AS c FROM tickets").fetchone()["c"] + except Exception: + pass + log.info("Ops tickets DB ready at %s (%s tickets)", DB_PATH, n) async def fetch_gpu() -> dict: @@ -673,13 +2255,14 @@ async def gpu_loop(): @app.on_event("startup") async def startup(): init_db() + _backup_ops_db("startup") asyncio.create_task(poll_loop()) asyncio.create_task(gpu_loop()) asyncio.create_task(warm_reports_cache()) async def warm_reports_cache(): - """Background warm of warranty/compliance for chat + inspector.""" + """Background warm of warranty/compliance/memory for chat + inspector.""" await asyncio.sleep(8) try: await fetch_warranties() @@ -691,6 +2274,20 @@ async def warm_reports_cache(): ) except Exception as e: log.warning("Reports cache warm failed: %s", e) + # Wait until fleet snapshot exists, then warm memory inventory for chat tools + for _ in range(40): + if STATE.get("devices"): + break + await asyncio.sleep(2) + try: + mem = await fetch_fleet_memory(force=False) + log.info( + "Memory cache warmed: %s/%s servers", + mem.get("with_memory"), + mem.get("count"), + ) + except Exception as e: + log.warning("Memory cache warm failed: %s", e) @app.get("/api/health") @@ -813,6 +2410,7 @@ async def device_detail(device_id: int): "drivers": drivers, "applications": applications, } + detail["expansion"] = build_expansion_report(node.get("model"), detail.get("inventory") or {}) finally: if sid: try: @@ -827,6 +2425,18 @@ async def device_detail(device_id: int): return detail +@app.get("/api/devices/{device_id}/expansion") +async def device_expansion(device_id: int, force: bool = False): + if force and device_id in DETAIL_CACHE: + DETAIL_CACHE.pop(device_id, None) + detail = await device_detail(device_id) + return { + "device": detail.get("device"), + "expansion": detail.get("expansion") + or build_expansion_report((detail.get("device") or {}).get("model"), detail.get("inventory") or {}), + } + + @app.websocket("/ws/fleet") async def ws_fleet(ws: WebSocket): await ws.accept() @@ -868,6 +2478,205 @@ class TicketPatch(BaseModel): accepted_by: str | None = None +def _device_by_id(device_id: int | None) -> dict | None: + if device_id is None: + return None + return next((d for d in (STATE.get("devices") or []) if d.get("id") == device_id), None) + + +def _device_by_service_tag(st: str) -> dict | None: + needle = (st or "").strip().upper() + if not needle: + return None + for d in STATE.get("devices") or []: + if (d.get("service_tag") or "").strip().upper() == needle: + return d + return None + + +def _find_devices_fuzzy(query: str, limit: int = 8) -> list[dict]: + q = (query or "").strip().lower() + if not q: + return [] + hits = [] + for d in STATE.get("devices") or []: + blob = " ".join( + str(d.get(k) or "").lower() + for k in ("name", "service_tag", "ip", "idrac_ip", "rdp_host", "os_hostname", "model") + ) + if q in blob: + hits.append(d) + hits.sort(key=lambda d: (0 if (d.get("service_tag") or "").lower() == q else 1, d.get("name") or "")) + return hits[:limit] + + +def _format_device_card(d: dict) -> str: + mem = d.get("memory_gb") + mem_s = f"{mem}GB/{d.get('dimm_count') or '?'}DIMM" if mem is not None else "unknown" + return ( + "ST={st} | name={name} | model={model} | idrac={idrac} | " + "rdp={rdp} | os_host={osh} | ram={ram} | connected={conn} | status={status} | watts={watts}" + ).format( + st=(d.get("service_tag") or "NONE"), + name=(d.get("name") or "")[:48], + model=(d.get("model") or "")[:36], + idrac=d.get("idrac_ip") or d.get("ip") or "—", + rdp=(",".join(d.get("rdp_ips") or []) or d.get("rdp_host") or "unresolved"), + osh=d.get("os_hostname") or "—", + ram=mem_s, + conn="yes" if d.get("connected") else "no", + status=d.get("status"), + watts=d.get("watts"), + ) + + +async def ome_fetch_inventory_types(device_id: int, types: list[str]) -> dict[str, list]: + """Fast parallel inventory fetch for chat tools (not full device landscape).""" + inventory: dict[str, list] = {} + async with httpx.AsyncClient(verify=False, timeout=45.0) as client: + base, headers, sid = await ome_session(client) + try: + + async def one(inv_type: str): + try: + ir = await client.get( + f"{base}/api/DeviceService/Devices({device_id})/InventoryDetails('{inv_type}')", + headers=headers, + ) + if ir.status_code == 200: + info = ir.json().get("InventoryInfo") or [] + if info: + return inv_type, info + except Exception as e: + log.debug("chat inv %s failed: %s", inv_type, e) + return inv_type, [] + + results = await asyncio.gather(*[one(t) for t in types]) + for inv_type, info in results: + if info: + inventory[inv_type] = info + finally: + await ome_session_delete(client, base, headers, sid) + return inventory + + + +def _memory_from_dimms(mem_raw: list) -> dict: + """Compact CURRENT memory summary from OME serverMemoryDevices.""" + dimms = 0 + total_gb = 0.0 + for dimm in mem_raw or []: + gb = _mem_size_gb(dimm.get("Size")) + if gb is None: + continue + dimms += 1 + total_gb += gb + return { + "memory_gb": round(total_gb, 1) if dimms else None, + "dimm_count": dimms or None, + } + + +async def fetch_fleet_memory(force: bool = False) -> dict: + """Warm/cache CURRENT installed RAM for all managed servers (OME inventory).""" + cached = None if force else _cache_get("fleet_memory") + if cached is not None: + return cached + + devices = [ + d + for d in (STATE.get("devices") or []) + if d.get("is_server") or d.get("is_idrac") or (d.get("type") == 1000) + ] + # Prefer unique device ids + seen: set[int] = set() + targets: list[dict] = [] + for d in devices: + did = d.get("id") + if did is None or did in seen: + continue + seen.add(int(did)) + targets.append(d) + + rows: list[dict] = [] + errors = 0 + sem = asyncio.Semaphore(10) + + async with httpx.AsyncClient(verify=False, timeout=60.0) as client: + base, headers, sid = await ome_session(client) + try: + + async def one(node: dict): + nonlocal errors + did = int(node["id"]) + async with sem: + try: + ir = await client.get( + f"{base}/api/DeviceService/Devices({did})/InventoryDetails('serverMemoryDevices')", + headers=headers, + ) + mem_raw = [] + if ir.status_code == 200: + mem_raw = ir.json().get("InventoryInfo") or [] + stats = _memory_from_dimms(mem_raw) + return { + "id": did, + "service_tag": node.get("service_tag"), + "name": node.get("name"), + "model": node.get("model"), + "idrac_ip": node.get("idrac_ip") or node.get("ip"), + "connected": bool(node.get("connected")), + "memory_gb": stats.get("memory_gb"), + "dimm_count": stats.get("dimm_count"), + } + except Exception as e: + errors += 1 + log.debug("fleet memory %s: %s", did, e) + return { + "id": did, + "service_tag": node.get("service_tag"), + "name": node.get("name"), + "model": node.get("model"), + "idrac_ip": node.get("idrac_ip") or node.get("ip"), + "connected": bool(node.get("connected")), + "memory_gb": None, + "dimm_count": None, + "error": str(e)[:80], + } + + rows = list(await asyncio.gather(*[one(n) for n in targets])) + finally: + await ome_session_delete(client, base, headers, sid) + + rows.sort(key=lambda r: ((r.get("name") or "").lower(), r.get("service_tag") or "")) + known = [r for r in rows if r.get("memory_gb") is not None] + payload = { + "source": "OME DeviceService InventoryDetails(serverMemoryDevices)", + "count": len(rows), + "with_memory": len(known), + "errors": errors, + "total_memory_gb": round(sum(r.get("memory_gb") or 0 for r in known), 1), + "servers": rows, + "updated_at": time.time(), + } + _cache_set("fleet_memory", payload) + # also stamp onto fleet nodes for UI/context + by_id = {r["id"]: r for r in rows if r.get("id") is not None} + for d in STATE.get("devices") or []: + hit = by_id.get(d.get("id")) + if hit: + d["memory_gb"] = hit.get("memory_gb") + d["dimm_count"] = hit.get("dimm_count") + log.info( + "Fleet memory cache: %s/%s servers with RAM (%.0f GB total)", + len(known), + len(rows), + payload["total_memory_gb"], + ) + return payload + + + def build_fleet_context(focus_device_id: int | None = None, max_chars: int | None = None) -> str: summary = STATE.get("summary") or {} gpu = STATE.get("gpu") or {} @@ -885,19 +2694,24 @@ def build_fleet_context(focus_device_id: int | None = None, max_chars: int | Non hottest = (ctx.get("hottest") or [])[:5] compliance = _cache_get("compliance") or REPORT_CACHE.get("compliance") or {} comp_sum = (compliance or {}).get("summary") or {} + by_id = {d.get("id"): d for d in devices if d.get("id") is not None} - # SERVICE TAG INDEX first — must survive truncation st_lines = [ - "SERVICE TAG INDEX (use these Service Tags in every node answer):", + "SERVICE TAG INDEX (canonical). idrac=BMC management IP. rdp=OS/Windows RDP candidates (DNS). Never call idrac the RDP IP.", ] for d in sorted(devices, key=lambda x: (x.get("name") or "").lower()): st = (d.get("service_tag") or "").strip() or "NONE" + rdp = ",".join((d.get("rdp_ips") or [])[:2]) or (d.get("rdp_host") or "—") + mem = d.get("memory_gb") + ram = f"{mem}GB" if mem is not None else "?" st_lines.append( - "- ST={st} | {name} | ip={ip} | model={model} | connected={conn}".format( + "- ST={st} | {name} | idrac={idrac} | rdp={rdp} | model={model} | ram={ram} | connected={conn}".format( st=st, - name=(d.get("name") or "")[:40], - ip=d.get("ip") or "—", - model=(d.get("model") or "")[:28], + name=(d.get("name") or "")[:36], + idrac=d.get("idrac_ip") or d.get("ip") or "—", + rdp=rdp[:40], + model=(d.get("model") or "")[:26], + ram=ram, conn="yes" if d.get("connected") else "no", ) ) @@ -906,9 +2720,12 @@ def build_fleet_context(focus_device_id: int | None = None, max_chars: int | Non lines = [ "You are OpenManage Cockpit Copilot for Dell ATC. Be concise and operational.", "Admins: Jody van Dongen, Laurens Rammers, Mohamed El Kadi.", - "Use ONLY this live snapshot. Unknown => say unknown.", - "ALWAYS include Service Tag (ST=...), model, and IP when discussing any system.", - "If a Service Tag is missing in the index, say so explicitly.", + "ACCURACY RULES (mandatory):", + "1) Use ONLY facts from this snapshot and any TOOL FACTS block. If missing => say unknown. Never invent ST, IPs, DIMM counts, firmware versions, or RDP targets.", + "2) ALWAYS cite Service Tag (ST=...), model, idrac IP, and rdp IP (or 'rdp unresolved') when discussing a system.", + "3) Management/iDRAC IP is NOT the Windows RDP IP. Never recommend RDP to an iDRAC address.", + "4) Hardware CURRENT (OME inventory) is NOT Dell CATALOG MAX. Never state catalog maxima as installed capacity.", + "5) Prefer TOOL FACTS over the summary index when both are present.", "", "FLEET: total={t} connected={c} offline={o} watts={w} alerts_total={a}".format( t=summary.get("total"), @@ -920,25 +2737,48 @@ def build_fleet_context(focus_device_id: int | None = None, max_chars: int | Non ] if comp_sum: lines.append( - "FIRMWARE COMPLIANCE (Dell catalog baseline): outdated_devices={od} critical_components={cc} baseline={bn}".format( + "FIRMWARE COMPLIANCE summary (Dell baseline): outdated_devices={od} critical_components={cc} baseline={bn}".format( od=comp_sum.get("outdated_devices"), cc=comp_sum.get("critical_components"), bn=(comp_sum.get("baseline_name") or "")[:40], ) ) - lines.append("CONNECTED:") - for d in connected[:18]: + mem_cache = _cache_get("fleet_memory") or REPORT_CACHE.get("fleet_memory") or {} + mem_servers = mem_cache.get("servers") or [] + if mem_servers: lines.append( - "- ST={st} {name} ip={ip} model={model} W={watts} status={status}".format( - st=(d.get("service_tag") or "NONE"), - name=(d.get("name") or "")[:36], - ip=d.get("ip"), - model=(d.get("model") or "")[:28], - watts=d.get("watts"), - status=d.get("status"), + "MEMORY INDEX (CURRENT OME inventory, GB installed): known={k}/{n} total_gb={tg}".format( + k=mem_cache.get("with_memory"), + n=mem_cache.get("count"), + tg=mem_cache.get("total_memory_gb"), ) ) + ranked = sorted( + [r for r in mem_servers if r.get("memory_gb") is not None], + key=lambda r: (0 if r.get("connected") else 1, (r.get("name") or "").lower()), + ) + for r in ranked[:55]: + lines.append( + "- ST={st} {name} model={model} ram={gb}GB dimms={dc} idrac={ip}".format( + st=r.get("service_tag") or "NONE", + name=(r.get("name") or "")[:28], + model=(r.get("model") or "")[:22], + gb=r.get("memory_gb"), + dc=r.get("dimm_count") or "?", + ip=r.get("idrac_ip") or "—", + ) + ) + if len(ranked) > 55: + lines.append(f"- … +{len(ranked) - 55} more in TOOL FACTS list_fleet_memory") + else: + lines.append( + "MEMORY INDEX: not warmed yet — call list_fleet_memory tool / wait for background warm." + ) + + lines.append("CONNECTED (sample):") + for d in connected[:14]: + lines.append("- " + _format_device_card(d)) lines.append("SUBNETS:") for s in subnets[:8]: @@ -951,27 +2791,25 @@ def build_fleet_context(focus_device_id: int | None = None, max_chars: int | Non ) ) + def _alert_line(a: dict, msg_len: int) -> str: + node = by_id.get(a.get("device_id")) or {} + st = node.get("service_tag") or a.get("service_tag") or "NONE" + return "- ST={st} {dev} idrac={ip}: {msg}".format( + st=st, + dev=(a.get("device") or node.get("name") or "")[:28], + ip=a.get("ip") or node.get("idrac_ip") or node.get("ip") or "—", + msg=(a.get("message") or "")[:msg_len], + ) + lines.append("CRITICAL:") if not critical: lines.append("(none)") for a in critical: - lines.append( - "- {dev} ({ip}): {msg}".format( - dev=(a.get("device") or "")[:32], - ip=a.get("ip"), - msg=(a.get("message") or "")[:110], - ) - ) + lines.append(_alert_line(a, 100)) lines.append("WARNINGS:") for a in warnings: - lines.append( - "- {dev} ({ip}): {msg}".format( - dev=(a.get("device") or "")[:32], - ip=a.get("ip"), - msg=(a.get("message") or "")[:90], - ) - ) + lines.append(_alert_line(a, 80)) lines.append("DELTAS:") if not events: @@ -981,7 +2819,14 @@ def build_fleet_context(focus_device_id: int | None = None, max_chars: int | Non lines.append("HOT POWER:") for h in hottest: - lines.append("- {name}: {watts}W".format(name=(h.get("name") or "")[:28], watts=h.get("watts"))) + node = by_id.get(h.get("id")) or {} + lines.append( + "- ST={st} {name}: {watts}W".format( + st=(node.get("service_tag") or h.get("service_tag") or "NONE"), + name=(h.get("name") or node.get("name") or "")[:28], + watts=h.get("watts"), + ) + ) lines.append( "GPU atc-gpu-prod: avg_util={u}% power={p}W mem={mu}/{mt}MB".format( @@ -1002,31 +2847,20 @@ def build_fleet_context(focus_device_id: int | None = None, max_chars: int | Non ) if focus_device_id is not None: - node = next((d for d in devices if d.get("id") == focus_device_id), None) - lines.append("FOCUS:") + node = _device_by_id(focus_device_id) + lines.append("FOCUS DEVICE:") if node: - lines.append( - "ST={st} {name} id={id} ip={ip} model={model} W={watts} connected={conn} status={stt}".format( - st=(node.get("service_tag") or "NONE"), - name=node.get("name"), - id=node.get("id"), - ip=node.get("ip"), - model=node.get("model"), - watts=node.get("watts"), - conn=node.get("connected"), - stt=node.get("status"), - ) - ) + lines.append(_format_device_card(node)) related = [a for a in alerts if a.get("device_id") == focus_device_id][:5] for a in related: lines.append( "alert {sev}: {msg}".format(sev=a.get("severity"), msg=(a.get("message") or "")[:100]) ) else: - lines.append("device_id=%s missing" % focus_device_id) + lines.append("device_id=%s missing from fleet snapshot" % focus_device_id) lines.append( - "Reply with concrete Service Tags, names, IPs, models, and next actions." + "Reply with concrete Service Tags, idrac/rdp IPs, models, and next actions. Quote TOOL FACTS verbatim for hardware numbers." ) body = "\n".join(lines) budget = max(800, limit - len(st_block) - 40) @@ -1038,6 +2872,396 @@ def build_fleet_context(focus_device_id: int | None = None, max_chars: int | Non return out +def plan_chat_tools(message: str, focus_device_id: int | None = None) -> list[tuple[str, dict]]: + """Server-side tool planner — Llama3-GPTQ does not emit native tool_calls reliably.""" + import re + + msg = message or "" + low = msg.lower() + planned: list[tuple[str, dict]] = [] + seen: set[str] = set() + + def add(name: str, args: dict): + key = name + json.dumps(args, sort_keys=True, default=str) + if key in seen: + return + seen.add(key) + planned.append((name, args)) + + for st in re.findall(r"\b([A-Z0-9]{7})\b", msg.upper()): + if st in {"OMEPROD", "WINDOWS", "POWERED", "UNKNOWN", "SERVICE", "CONNECTED"}: + continue + if _device_by_service_tag(st): + add("lookup_device", {"query": st}) + + for m in re.finditer(r"\b(?:for|on|about|host|server|node)\s+([A-Za-z0-9._-]{4,40})", msg, re.I): + add("lookup_device", {"query": m.group(1)}) + + wants_hw = bool( + re.search( + r"\b(memory|ram|dimm|disk|drive|ssd|hdd|pcie|expansion|cpu|processor|core|slot|bay|installed|capacity)\b", + low, + ) + ) + wants_rdp = bool(re.search(r"\b(rdp|remote\s*desktop|windows|hyper-?v|os\s*ip|hostname)\b", low)) + wants_fw = bool(re.search(r"\b(firmware|bios\s*version|outdated|compliance|firmware\s*catalog|update\s*catalog|baseline)\b", low)) + wants_war = bool(re.search(r"\b(warrant|prosupport|support\s*end|days?\s*left)\b", low)) + wants_alerts = bool(re.search(r"\b(alert|critical|warning|fault|health)\b", low)) + + target_id = focus_device_id + target_st = None + if planned: + q0 = planned[0][1].get("query") + node = _device_by_service_tag(str(q0)) or ((_find_devices_fuzzy(str(q0), 1) or [None])[0]) + if node: + target_id = node.get("id") + target_st = node.get("service_tag") + + if wants_rdp and not any(n == "lookup_device" for n, _ in planned): + if focus_device_id: + node = _device_by_id(focus_device_id) + if node: + add("lookup_device", {"query": node.get("service_tag") or node.get("name") or str(focus_device_id)}) + else: + n_lookups = 0 + for d in STATE.get("devices") or []: + if d.get("is_windows") or d.get("rdp_host"): + add("lookup_device", {"query": d.get("service_tag") or d.get("name")}) + n_lookups += 1 + if n_lookups >= 4: + break + + wants_fleet_mem = bool( + re.search( + r"\b(each|every|all|per\s+server|fleet|overview|elk|alle|iedere|elke)\b", + low, + ) + ) or bool(re.search(r"hoeveel\s+(memory|ram|geheugen)", low)) + + if wants_hw: + args: dict = {} + if target_id: + args["device_id"] = int(target_id) + if target_st: + args["service_tag"] = target_st + if args: + add("get_hardware", args) + if wants_fleet_mem: + add("list_fleet_memory", {}) + elif wants_fleet_mem or not focus_device_id: + add("list_fleet_memory", {}) + elif focus_device_id: + add("get_hardware", {"device_id": int(focus_device_id)}) + + if wants_fw: + args = {"outdated_only": True} + if target_id: + args["device_id"] = int(target_id) + if target_st: + args["service_tag"] = target_st + add("get_compliance", args) + + if wants_war: + args = {} + if target_id: + args["device_id"] = int(target_id) + if target_st: + args["service_tag"] = target_st + if args: + add("get_warranty", args) + elif focus_device_id: + add("get_warranty", {"device_id": int(focus_device_id)}) + + if wants_alerts: + args = {} + if target_id: + args["device_id"] = int(target_id) + if target_st: + args["service_tag"] = target_st + add("list_alerts", args) + + if focus_device_id and not planned: + node = _device_by_id(focus_device_id) + if node: + add("lookup_device", {"query": node.get("service_tag") or str(focus_device_id)}) + + return planned[:6] + + +async def run_chat_tool(name: str, args: dict) -> dict: + args = args or {} + try: + if name == "lookup_device": + q = str(args.get("query") or "").strip() + exact = _device_by_service_tag(q) + hits = [exact] if exact else _find_devices_fuzzy(q, 8) + return { + "tool": name, + "query": q, + "count": len(hits), + "devices": [ + { + "id": d.get("id"), + "service_tag": d.get("service_tag"), + "name": d.get("name"), + "model": d.get("model"), + "idrac_ip": d.get("idrac_ip") or d.get("ip"), + "rdp_host": d.get("rdp_host"), + "rdp_ips": d.get("rdp_ips") or [], + "os_hostname": d.get("os_hostname"), + "is_windows": d.get("is_windows"), + "connected": d.get("connected"), + "status": d.get("status"), + "watts": d.get("watts"), + } + for d in hits + if d + ], + } + + if name == "get_hardware": + node = None + if args.get("device_id") is not None: + node = _device_by_id(int(args["device_id"])) + if not node and args.get("service_tag"): + node = _device_by_service_tag(str(args["service_tag"])) + if not node: + return {"tool": name, "error": "device not found in fleet snapshot"} + did = int(node["id"]) + inv = await ome_fetch_inventory_types( + did, + [ + "serverMemoryDevices", + "serverProcessors", + "serverArrayDisks", + "serverRaidControllers", + "serverDeviceCards", + "serverOperatingSystems", + ], + ) + report = build_expansion_report(node.get("model"), inv) + cur = report.get("current") or {} + exp = report.get("expansion") or {} + return { + "tool": name, + "device": { + "id": did, + "service_tag": node.get("service_tag"), + "name": node.get("name"), + "model": node.get("model"), + "idrac_ip": node.get("idrac_ip") or node.get("ip"), + "rdp_ips": node.get("rdp_ips") or [], + "os_hostname": node.get("os_hostname"), + }, + "current_ome_inventory": { + "memory_installed_gb": (cur.get("memory") or {}).get("installed_gb"), + "dimm_count": (cur.get("memory") or {}).get("dimm_count"), + "dimm_modules": [ + { + "slot": m.get("slot"), + "size_gb": m.get("size_gb"), + "rated_mts": m.get("rated_mts"), + "operating_mts": m.get("operating_mts"), + } + for m in ((cur.get("memory") or {}).get("modules") or [])[:24] + ], + "cpu_sockets_populated": (cur.get("cpu") or {}).get("sockets_populated"), + "cpu_cores_total": (cur.get("cpu") or {}).get("cores_total"), + "processors": (cur.get("cpu") or {}).get("processors") or [], + "disk_count": (cur.get("disks") or {}).get("count"), + "disk_capacity_gb": (cur.get("disks") or {}).get("capacity_gb"), + "disks": [ + { + "bay": d.get("bay"), + "model": d.get("model"), + "size_gb": d.get("size_gb"), + "bus": d.get("bus"), + "media": d.get("media"), + } + for d in ((cur.get("disks") or {}).get("items") or [])[:20] + ], + "os": cur.get("os") or [], + }, + "catalog_max_not_measured": { + "source": exp.get("source"), + "catalog_known": exp.get("catalog_known"), + "catalog_model": exp.get("catalog_model"), + "memory_max_tb": (exp.get("memory") or {}).get("max_capacity_tb"), + "dimm_slots_max": (exp.get("memory") or {}).get("dimm_slots_max"), + "cpu_sockets_max": (exp.get("cpu") or {}).get("sockets_max"), + "drive_bays_max": (exp.get("disks") or {}).get("bays_max"), + "pcie_slots_max": (exp.get("pcie") or {}).get("slots_max"), + }, + "findings": report.get("findings") or [], + } + + if name == "list_fleet_memory": + mem = await fetch_fleet_memory(force=bool(args.get("force"))) + servers = mem.get("servers") or [] + # Compact rows for the LLM — include unknown so the model does not invent + slim = [ + { + "service_tag": s.get("service_tag"), + "name": s.get("name"), + "model": s.get("model"), + "idrac_ip": s.get("idrac_ip"), + "connected": s.get("connected"), + "memory_gb": s.get("memory_gb"), + "dimm_count": s.get("dimm_count"), + } + for s in servers + ] + return { + "tool": name, + "source": mem.get("source"), + "count": mem.get("count"), + "with_memory": mem.get("with_memory"), + "total_memory_gb": mem.get("total_memory_gb"), + "note": "memory_gb is CURRENT installed RAM from OME inventory (not catalog max).", + "servers": slim, + } + + if name == "get_compliance": + compliance = await fetch_compliance(force=False) + comps = compliance.get("components") or [] + node = None + if args.get("device_id") is not None: + node = _device_by_id(int(args["device_id"])) + if not node and args.get("service_tag"): + node = _device_by_service_tag(str(args["service_tag"])) + outdated_only = bool(args.get("outdated_only", True)) + if node: + did = node.get("id") + st = (node.get("service_tag") or "").upper() + rows = [ + c + for c in comps + if c.get("device_id") == did + or (st and (c.get("service_tag") or "").upper() == st) + ] + else: + rows = list(comps) + if outdated_only: + rows = [ + c + for c in rows + if str(c.get("compliance_status") or "").lower() + in ("critical", "warning", "downgrade", "non-compliant", "outdated") + or ( + str(c.get("update_action") or "").lower() + not in ("", "equal", "compliant", "ok", "none") + and str(c.get("current_version") or "") != str(c.get("catalog_version") or "") + ) + ] + slim = [ + { + "service_tag": c.get("service_tag"), + "device_name": c.get("device_name"), + "component": c.get("component"), + "current_version": c.get("current_version"), + "catalog_version": c.get("catalog_version"), + "compliance_status": c.get("compliance_status"), + "update_action": c.get("update_action"), + } + for c in rows[:40] + ] + return {"tool": name, "summary": compliance.get("summary"), "count": len(slim), "components": slim} + + if name == "get_warranty": + warranties = await fetch_warranties(force=False) + items = warranties.get("items") or warranties.get("warranties") or [] + node = None + if args.get("device_id") is not None: + node = _device_by_id(int(args["device_id"])) + if not node and args.get("service_tag"): + node = _device_by_service_tag(str(args["service_tag"])) + if not node: + return {"tool": name, "error": "device not found"} + st = (node.get("service_tag") or "").upper() + rows = [ + w + for w in items + if w.get("device_id") == node.get("id") + or (st and (w.get("service_tag") or "").upper() == st) + ] + return { + "tool": name, + "service_tag": node.get("service_tag"), + "name": node.get("name"), + "warranties": rows[:8], + } + + if name == "list_alerts": + alerts = STATE.get("alerts") or [] + by_id = {d.get("id"): d for d in (STATE.get("devices") or [])} + node = None + if args.get("device_id") is not None: + node = _device_by_id(int(args["device_id"])) + if not node and args.get("service_tag"): + node = _device_by_service_tag(str(args["service_tag"])) + sev = (args.get("severity") or "").strip().lower() + out = [] + for a in alerts: + if node and a.get("device_id") != node.get("id"): + continue + if sev and str(a.get("severity") or "").lower() != sev: + continue + d = by_id.get(a.get("device_id")) or {} + out.append( + { + "severity": a.get("severity"), + "service_tag": d.get("service_tag"), + "device": a.get("device") or d.get("name"), + "idrac_ip": a.get("ip") or d.get("idrac_ip") or d.get("ip"), + "message": (a.get("message") or "")[:180], + } + ) + if len(out) >= 25: + break + return {"tool": name, "count": len(out), "alerts": out} + + return {"tool": name, "error": f"unknown tool {name}"} + except Exception as e: + log.exception("chat tool %s failed", name) + return {"tool": name, "error": str(e)} + + +def format_tool_facts(results: list[dict]) -> str: + if not results: + return "" + parts = ["TOOL FACTS (live lookups — authoritative; do not contradict):"] + for r in results: + if r.get("tool") == "list_fleet_memory" and r.get("servers"): + lines = [ + "list_fleet_memory CURRENT RAM:", + f"known={r.get('with_memory')}/{r.get('count')} total_gb={r.get('total_memory_gb')}", + "ST\tname\tmodel\tmemory_gb\tdimms\tidrac", + ] + for s in r.get("servers") or []: + lines.append( + "{st}\t{name}\t{model}\t{gb}\t{dc}\t{ip}".format( + st=s.get("service_tag") or "NONE", + name=(s.get("name") or "")[:36], + model=(s.get("model") or "")[:24], + gb=s.get("memory_gb") if s.get("memory_gb") is not None else "unknown", + dc=s.get("dimm_count") if s.get("dimm_count") is not None else "unknown", + ip=s.get("idrac_ip") or "—", + ) + ) + parts.append("\n".join(lines)[:14000]) + else: + parts.append(json.dumps(r, ensure_ascii=False, default=str)[:14000]) + return "\n".join(parts) + + + + + +@app.get("/api/fleet-memory") +async def api_fleet_memory(force: bool = False): + """CURRENT installed RAM per server (OME inventory cache).""" + return await fetch_fleet_memory(force=force) + @app.get("/api/gpu") async def api_gpu(): @@ -1147,20 +3371,126 @@ async def api_models(): return {"models": models, "default": settings.vllm_model} + +def format_fleet_memory_reply(result: dict) -> str: + """Deterministic full table — LLMs truncate long fleet lists.""" + servers = result.get("servers") or [] + lines = [ + "CURRENT installed RAM from OME inventory (not Dell catalog max).", + "known={k}/{n} · total_installed={tg} GB".format( + k=result.get("with_memory"), + n=result.get("count"), + tg=result.get("total_memory_gb"), + ), + "", + "ST | name | model | memory_gb | dimms | idrac | connected", + ] + for s in servers: + gb = s.get("memory_gb") + gb_s = "unknown" if gb is None else str(gb) + dc = s.get("dimm_count") + dc_s = "unknown" if dc is None else str(dc) + lines.append( + "{st} | {name} | {model} | {gb} | {dc} | {ip} | {conn}".format( + st=s.get("service_tag") or "NONE", + name=(s.get("name") or "")[:40], + model=(s.get("model") or "")[:28], + gb=gb_s, + dc=dc_s, + ip=s.get("idrac_ip") or "—", + conn="yes" if s.get("connected") else "no", + ) + ) + return "\n".join(lines) + + +def should_use_deterministic_fleet_memory(message: str, planned: list, tool_results: list[dict]) -> dict | None: + if not any(n == "list_fleet_memory" for n, _ in planned): + return None + # Prefer deterministic when the ask is clearly a full memory inventory + low = (message or "").lower() + inventoryish = bool( + re.search(r"\b(memory|ram|geheugen|dimm)\b", low) + and re.search(r"\b(elk|elke|iedere|alle|each|every|all|per\s+server|hoeveel|list|overview)\b", low) + ) + # Or when list_fleet_memory is the only tool + only_mem = len(planned) == 1 and planned[0][0] == "list_fleet_memory" + if not (inventoryish or only_mem): + return None + for r in tool_results: + if r.get("tool") == "list_fleet_memory" and not r.get("error"): + return r + return None + + @app.post("/api/chat") async def api_chat(payload: ChatIn): gpu = STATE.get("gpu") or {} gsum = gpu.get("summary") or {} model = (payload.model or settings.vllm_model or "").strip() or settings.vllm_model + # 1) Plan + execute live tools BEFORE the LLM (accurate facts; Llama3-GPTQ lacks native tool_calls) + planned = plan_chat_tools(payload.message, payload.focus_device_id) + tool_results: list[dict] = [] + if planned: + tool_results = list( + await asyncio.gather(*[run_chat_tool(name, args) for name, args in planned]) + ) + + max_tokens = settings.vllm_max_tokens + if any(n == "list_fleet_memory" for n, _ in planned): + max_tokens = max(max_tokens, 1800) + + # Full fleet memory lists: answer from TOOL FACTS directly (accurate, complete) + det = should_use_deterministic_fleet_memory(payload.message, planned, tool_results) + if det is not None: + gpu = STATE.get("gpu") or {} + gsum = gpu.get("summary") or {} + return { + "reply": format_fleet_memory_reply(det), + "model": model, + "backend": "ome-tools", + "gpu": gsum, + "context_bytes": 0, + "tools_used": [{"name": n, "args": a} for n, a in planned], + "tool_count": len(tool_results), + "deterministic": True, + } + system = build_fleet_context(payload.focus_device_id, max_chars=settings.chat_system_chars) + facts = format_tool_facts(tool_results) + if facts: + # Prefer tool facts; keep within budget (vLLM context is tight) + budget = max(1800, settings.chat_system_chars - 100) + # Accuracy rules + short fleet head, then authoritative TOOL FACTS + head = system + if len(system) > 2800: + head = system[:2800] + "\n…[fleet truncated for tools]" + combined = head + "\n\n" + facts + if len(combined) > budget: + # Keep TOOL FACTS intact; shrink head further + max_facts = min(len(facts), budget - 900) + facts_trim = facts[:max_facts] + head = system[: max(600, budget - len(facts_trim) - 40)] + combined = head + "\n…\n\n" + facts_trim + system = combined[:budget] + messages = [{"role": "system", "content": system}] for h in (payload.history or [])[-4:]: role = h.get("role") content = h.get("content") if role in ("user", "assistant") and content: messages.append({"role": role, "content": str(content)[:1200]}) - messages.append({"role": "user", "content": payload.message[:2500]}) + user_msg = payload.message[:2500] + if tool_results: + user_msg += ( + "\n\n[System note: live TOOL FACTS were fetched for this question. " + "Treat TOOL FACTS as ground truth. Quote CURRENT vs CATALOG separately. " + "Do not hedge or invent. Say unknown only if a field is absent from TOOL FACTS. " + "For fleet lists: output EVERY row from TOOL FACTS in a compact table " + "(ST | name | model | memory_gb | dimms | idrac) — do not summarize or omit.]" + ) + messages.append({"role": "user", "content": user_msg}) owui_models = {"ome-copilot", "arena-model"} use_owui = model in owui_models @@ -1181,8 +3511,8 @@ async def api_chat(payload: ChatIn): json={ "model": model, "messages": messages, - "temperature": 0.2, - "max_tokens": settings.vllm_max_tokens, + "temperature": 0.1, + "max_tokens": max_tokens, "stream": False, }, ) @@ -1198,21 +3528,23 @@ async def api_chat(payload: ChatIn): json={ "model": model, "messages": messages, - "temperature": 0.2, - "max_tokens": settings.vllm_max_tokens, + "temperature": 0.1, + "max_tokens": max_tokens, }, ) if r.status_code >= 400: detail = r.text if r.status_code == 400 and "context length" in detail.lower(): - tight = build_fleet_context(payload.focus_device_id, max_chars=3200) + tight = build_fleet_context(payload.focus_device_id, max_chars=2800) + if facts: + tight = (tight + "\n\n" + facts)[:3800] messages = [{"role": "system", "content": tight}, messages[-1]] r = await client.post( url, json={ "model": model, "messages": messages, - "temperature": 0.2, + "temperature": 0.1, "max_tokens": 500, }, ) @@ -1232,6 +3564,8 @@ async def api_chat(payload: ChatIn): "backend": backend, "gpu": gsum, "context_bytes": len(system), + "tools_used": [{"name": n, "args": a} for n, a in planned], + "tool_count": len(tool_results), } except HTTPException: raise @@ -1285,6 +3619,7 @@ async def create_ticket(payload: TicketIn): (tid, payload.created_by, payload.body.strip()[:5000], now), ) row = conn.execute("SELECT * FROM tickets WHERE id=?", (tid,)).fetchone() + _backup_ops_db("create") return dict(row) @@ -1317,6 +3652,7 @@ async def add_ticket_message(ticket_id: int, payload: TicketMsgIn): "SELECT * FROM ticket_messages WHERE ticket_id=? ORDER BY created_at ASC", (ticket_id,), ).fetchall() + _backup_ops_db("message") return {"messages": [dict(m) for m in msgs]} @@ -1351,20 +3687,465 @@ async def patch_ticket(ticket_id: int, payload: TicketPatch): ), ) row = conn.execute("SELECT * FROM tickets WHERE id=?", (ticket_id,)).fetchone() + _backup_ops_db("patch") return dict(row) @app.delete("/api/tickets/{ticket_id}") async def delete_ticket(ticket_id: int): + _backup_ops_db("pre-delete") with _db() as conn: row = conn.execute("SELECT id FROM tickets WHERE id=?", (ticket_id,)).fetchone() if not row: raise HTTPException(404, "Ticket not found") conn.execute("DELETE FROM ticket_messages WHERE ticket_id=?", (ticket_id,)) conn.execute("DELETE FROM tickets WHERE id=?", (ticket_id,)) + _backup_ops_db("delete") return {"ok": True, "deleted": ticket_id} +class RackIn(BaseModel): + site_id: str + name: str + units: int = 42 + sort_order: int | None = None + + +class RackPatch(BaseModel): + name: str | None = None + sort_order: int | None = None + + +class PlacementIn(BaseModel): + rack_id: int + u_start: int + u_height: int | None = None + + +@app.get("/api/network/fabric") +async def api_network_fabric(): + return build_network_fabric() + + +class VlanIn(BaseModel): + vlan_id: int | None = None + name: str + cidr: str + purpose: str = "" + site_id: str | None = None + color: str = "#00a8e8" + sort_order: int = 0 + + +@app.get("/api/network/vlans") +async def api_network_vlans(): + return build_vlan_map() + + +@app.post("/api/network/vlans") +async def api_create_vlan(payload: VlanIn): + now = time.time() + try: + ipaddress.ip_network(payload.cidr, strict=False) + except Exception as e: + raise HTTPException(400, f"Invalid cidr: {e}") from e + with _db() as conn: + cur = conn.execute( + """ + INSERT INTO vlans(vlan_id, name, cidr, purpose, site_id, color, sort_order, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + payload.vlan_id, + payload.name.strip()[:80], + payload.cidr.strip(), + (payload.purpose or "")[:200], + payload.site_id, + payload.color or "#00a8e8", + payload.sort_order, + now, + now, + ), + ) + vid = cur.lastrowid + _backup_ops_db("vlan-create") + return {"id": vid, **build_vlan_map()} + + + +class PortLinkIn(BaseModel): + switch_port: int + device_id: int + device_port: str = "" + note: str = "" + + +@app.get("/api/network/switches/{switch_id}/ports") +async def api_switch_ports(switch_id: int): + return build_switch_portmap(switch_id) + + +@app.put("/api/network/switches/{switch_id}/ports/{port_num}") +async def api_put_port_link(switch_id: int, port_num: int, payload: PortLinkIn): + sw = next((d for d in (STATE.get("devices") or []) if d.get("id") == switch_id), None) + if not sw: + raise HTTPException(404, "Switch not found") + nports = _switch_port_count(sw) + if port_num < 1 or port_num > nports: + raise HTTPException(400, f"Port must be 1..{nports}") + peer = next((d for d in (STATE.get("devices") or []) if d.get("id") == payload.device_id), None) + if not peer: + raise HTTPException(404, "Target device not found in fleet") + now = time.time() + with _db() as conn: + conn.execute( + """ + INSERT INTO port_links(switch_id, switch_port, device_id, device_port, note, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(switch_id, switch_port) DO UPDATE SET + device_id=excluded.device_id, + device_port=excluded.device_port, + note=excluded.note, + updated_at=excluded.updated_at + """, + ( + switch_id, + port_num, + payload.device_id, + (payload.device_port or "")[:80], + (payload.note or "")[:200], + now, + ), + ) + _backup_ops_db("port-link") + return build_switch_portmap(switch_id) + + +@app.delete("/api/network/switches/{switch_id}/ports/{port_num}") +async def api_delete_port_link(switch_id: int, port_num: int): + with _db() as conn: + conn.execute( + "DELETE FROM port_links WHERE switch_id=? AND switch_port=?", + (switch_id, port_num), + ) + _backup_ops_db("port-unlink") + return build_switch_portmap(switch_id) + + +@app.get("/api/devices/{device_id}/nics") +async def api_device_nics(device_id: int): + """OME serverNetworkInterfaces for wiring UI (best-effort).""" + node = next((d for d in (STATE.get("devices") or []) if d.get("id") == device_id), None) + if not node: + raise HTTPException(404, "Device not found") + nics = [] + try: + inv = await ome_fetch_inventory_types(device_id, ["serverNetworkInterfaces"]) + for nic in inv.get("serverNetworkInterfaces") or []: + nics.append( + { + "name": nic.get("ProductName") or nic.get("DeviceDescription") or nic.get("Fqdd") or "NIC", + "fqdd": nic.get("Fqdd") or nic.get("InstanceId"), + "mac": nic.get("PermanentMACAddress") or nic.get("CurrentMACAddress"), + "speed": nic.get("LinkSpeed") or nic.get("Speed"), + "vendor": nic.get("Manufacturer") or nic.get("VendorName"), + } + ) + except Exception as e: + log.debug("nic fetch %s: %s", device_id, e) + return {"device_id": device_id, "nics": nics, "count": len(nics)} + + + +@app.get("/api/racks") +async def api_racks(): + _seed_atc_racks() + return _racks_payload() + + +@app.post("/api/racks") +async def api_create_rack(payload: RackIn): + now = time.time() + site = (payload.site_id or "").upper() + if site not in ("ATC1", "ATC2"): + raise HTTPException(400, "site_id must be ATC1 or ATC2") + with _db() as conn: + row = conn.execute("SELECT id FROM sites WHERE id=?", (site,)).fetchone() + if not row: + conn.execute( + "INSERT INTO sites(id, name, sort_order) VALUES (?, ?, ?)", + (site, site, 1 if site == "ATC1" else 2), + ) + so = payload.sort_order + if so is None: + mx = conn.execute( + "SELECT COALESCE(MAX(sort_order), 0) AS m FROM racks WHERE site_id=?", + (site,), + ).fetchone()["m"] + so = int(mx) + 1 + cur = conn.execute( + """ + INSERT INTO racks(site_id, name, units, sort_order, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + """, + (site, payload.name.strip()[:64], max(1, min(48, payload.units)), so, now, now), + ) + rid = cur.lastrowid + _backup_ops_db("rack-create") + return {"id": rid, **_racks_payload()} + + +@app.patch("/api/racks/{rack_id}") +async def api_patch_rack(rack_id: int, payload: RackPatch): + now = time.time() + with _db() as conn: + row = conn.execute("SELECT * FROM racks WHERE id=?", (rack_id,)).fetchone() + if not row: + raise HTTPException(404, "Rack not found") + name = payload.name.strip()[:64] if payload.name else row["name"] + so = payload.sort_order if payload.sort_order is not None else row["sort_order"] + conn.execute( + "UPDATE racks SET name=?, sort_order=?, updated_at=? WHERE id=?", + (name, so, now, rack_id), + ) + _backup_ops_db("rack-patch") + return _racks_payload() + + + +class RackItemIn(BaseModel): + rack_id: int + u_start: int = 1 + u_height: int | None = None + side: str = "front" # front|rear|left|right + device_id: int | None = None + catalog_sku: str | None = None + label: str | None = None + notes: str | None = None + + +class RackItemPatch(BaseModel): + rack_id: int | None = None + u_start: int | None = None + u_height: int | None = None + side: str | None = None + catalog_sku: str | None = None + label: str | None = None + notes: str | None = None + + +@app.get("/api/rack-catalog") +async def api_rack_catalog(): + families: dict[str, list] = {} + for item in DELL_RACK_CATALOG: + families.setdefault(item["family"], []).append(item) + return {"items": DELL_RACK_CATALOG, "families": families, "count": len(DELL_RACK_CATALOG)} + + +@app.put("/api/racks/placements/{device_id}") +async def api_put_placement(device_id: int, payload: PlacementIn): + """Place/move an OME device (compat). Writes rack_items.""" + device = next((d for d in (STATE.get("devices") or []) if d.get("id") == device_id), None) + cat = match_rack_catalog((device or {}).get("model"), (device or {}).get("role")) + body = RackItemIn( + rack_id=payload.rack_id, + u_start=payload.u_start, + u_height=payload.u_height if payload.u_height is not None else _default_u_height(device), + side="front", + device_id=device_id, + catalog_sku=(cat or {}).get("sku"), + label=(device or {}).get("name"), + ) + return await api_upsert_rack_item(body) + + +@app.delete("/api/racks/placements/{device_id}") +async def api_delete_placement(device_id: int): + with _db() as conn: + conn.execute("DELETE FROM rack_items WHERE device_id=?", (device_id,)) + try: + conn.execute("DELETE FROM rack_placements WHERE device_id=?", (device_id,)) + except Exception: + pass + _backup_ops_db("unplace") + return _racks_payload() + + +@app.post("/api/racks/items") +async def api_upsert_rack_item(payload: RackItemIn): + now = time.time() + side = (payload.side or "front").lower() + if side not in ("front", "rear", "left", "right"): + raise HTTPException(400, "side must be front|rear|left|right") + device = None + if payload.device_id is not None: + device = next((d for d in (STATE.get("devices") or []) if d.get("id") == payload.device_id), None) + if not device: + raise HTTPException(404, "OME device not found") + sku = payload.catalog_sku + cat = next((c for c in DELL_RACK_CATALOG if c["sku"] == sku), None) if sku else None + if not cat and device: + cat = match_rack_catalog(device.get("model"), device.get("role")) + sku = (cat or {}).get("sku") + if not device and not cat and not sku: + raise HTTPException(400, "Provide device_id or catalog_sku") + height = payload.u_height + # Prefer catalog U-height for known SKUs so 2U/3U/… always place correctly + if cat is not None: + cat_h = int(cat.get("u_height") or 0) + if cat_h > 0: + height = cat_h + elif height is None: + height = _default_u_height(device) + elif height is None: + height = _default_u_height(device) + if side in ("left", "right"): + u_start, height = 1, 42 + else: + height = max(1, min(42, int(height))) + u_start = int(payload.u_start) + if u_start < 1 or u_start + height - 1 > 42: + raise HTTPException(400, f"Must fit U1–U42 (u_start={u_start} height={height})") + label = (payload.label or (device or {}).get("name") or (cat or {}).get("name") or "Device")[:120] + with _db() as conn: + rack = conn.execute("SELECT * FROM racks WHERE id=?", (payload.rack_id,)).fetchone() + if not rack: + raise HTTPException(404, "Rack not found") + if _rack_overlap( + conn, + payload.rack_id, + u_start if side not in ("left", "right") else 1, + height, + side=side, + exclude_device_id=payload.device_id, + ): + raise HTTPException(409, "U range overlaps another item on this side") + existing = None + if payload.device_id is not None: + existing = conn.execute( + "SELECT id FROM rack_items WHERE device_id=?", (payload.device_id,) + ).fetchone() + if existing: + conn.execute( + """ + UPDATE rack_items + SET rack_id=?, catalog_sku=?, label=?, u_start=?, u_height=?, side=?, notes=?, updated_at=? + WHERE id=? + """, + ( + payload.rack_id, + sku, + label, + u_start if side not in ("left", "right") else 1, + height, + side, + (payload.notes or "")[:200], + now, + existing["id"], + ), + ) + item_id = existing["id"] + else: + cur = conn.execute( + """ + INSERT INTO rack_items(rack_id, device_id, catalog_sku, label, u_start, u_height, side, notes, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + payload.rack_id, + payload.device_id, + sku, + label, + u_start if side not in ("left", "right") else 1, + height, + side, + (payload.notes or "")[:200], + now, + ), + ) + item_id = cur.lastrowid + # keep legacy table in sync for OME devices + if payload.device_id is not None and side == "front": + try: + conn.execute( + """ + INSERT INTO rack_placements(device_id, rack_id, u_start, u_height, updated_at) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(device_id) DO UPDATE SET + rack_id=excluded.rack_id, u_start=excluded.u_start, + u_height=excluded.u_height, updated_at=excluded.updated_at + """, + (payload.device_id, payload.rack_id, u_start, height, now), + ) + except Exception: + pass + _backup_ops_db("rack-item") + payload_out = _racks_payload() + payload_out["item_id"] = item_id + return payload_out + + +@app.patch("/api/racks/items/{item_id}") +async def api_patch_rack_item(item_id: int, payload: RackItemPatch): + now = time.time() + with _db() as conn: + row = conn.execute("SELECT * FROM rack_items WHERE id=?", (item_id,)).fetchone() + if not row: + raise HTTPException(404, "Item not found") + rack_id = payload.rack_id if payload.rack_id is not None else row["rack_id"] + side = (payload.side or row["side"] or "front").lower() + u_start = payload.u_start if payload.u_start is not None else row["u_start"] + height = payload.u_height if payload.u_height is not None else row["u_height"] + if side in ("left", "right"): + u_start, height = 1, 42 + else: + height = max(1, min(42, int(height))) + u_start = int(u_start) + if u_start < 1 or u_start + height - 1 > 42: + raise HTTPException(400, "Must fit U1–U42") + if _rack_overlap(conn, rack_id, u_start, height, side=side, exclude_item_id=item_id): + raise HTTPException(409, "U range overlaps another item on this side") + conn.execute( + """ + UPDATE rack_items + SET rack_id=?, catalog_sku=COALESCE(?, catalog_sku), label=COALESCE(?, label), + u_start=?, u_height=?, side=?, notes=COALESCE(?, notes), updated_at=? + WHERE id=? + """, + ( + rack_id, + payload.catalog_sku, + payload.label, + u_start, + height, + side, + payload.notes, + now, + item_id, + ), + ) + _backup_ops_db("rack-item-patch") + return _racks_payload() + + +@app.delete("/api/racks/items/{item_id}") +async def api_delete_rack_item(item_id: int): + with _db() as conn: + row = conn.execute("SELECT * FROM rack_items WHERE id=?", (item_id,)).fetchone() + if not row: + raise HTTPException(404, "Item not found") + conn.execute("DELETE FROM rack_items WHERE id=?", (item_id,)) + if row["device_id"] is not None: + try: + conn.execute("DELETE FROM rack_placements WHERE device_id=?", (row["device_id"],)) + except Exception: + pass + _backup_ops_db("rack-item-delete") + return _racks_payload() + + + + def _get_ssh_sem() -> asyncio.Semaphore: global _ssh_sem @@ -1966,6 +4747,12 @@ def build_fleet_report_rows(warranties: dict | None = None) -> list[dict]: "service_tag": d.get("service_tag"), "model": d.get("model"), "ip": d.get("ip"), + "idrac_ip": d.get("idrac_ip") or d.get("ip"), + "os_hostname": d.get("os_hostname"), + "rdp_host": d.get("rdp_host"), + "rdp_ips": d.get("rdp_ips") or [], + "os_ips": d.get("os_ips") or [], + "is_windows": d.get("is_windows"), "subnet": d.get("subnet"), "type": d.get("type"), "sub_type": d.get("sub_type"), @@ -2233,11 +5020,80 @@ async def ops_js(): return FileResponse(STATIC_DIR / "ops.js", media_type="application/javascript") +@app.get("/network.js") +async def network_js(): + return FileResponse(STATIC_DIR / "network.js", media_type="application/javascript") + + @app.get("/ssh.js") async def ssh_js(): return FileResponse(STATIC_DIR / "ssh.js", media_type="application/javascript") +@app.get("/rdp.js") +async def rdp_js(): + return FileResponse(STATIC_DIR / "rdp.js", media_type="application/javascript") + + +@app.get("/rdp-popout.html") +async def rdp_popout_html(): + resp = FileResponse(STATIC_DIR / "rdp-popout.html") + resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate" + return resp + + +@app.get("/api/rdp.rdp") +async def api_rdp_file(host: str, port: int = 3389, username: str = ""): + """Download a standard .rdp file for the local Remote Desktop client.""" + host = (host or "").strip() + if not host or len(host) > 253 or any(c in host for c in "\r\n\x00"): + raise HTTPException(400, "Invalid host") + if port < 1 or port > 65535: + raise HTTPException(400, "Invalid port") + username = (username or "").strip() + if len(username) > 256 or any(c in username for c in "\r\n\x00"): + raise HTTPException(400, "Invalid username") + full = f"{host}:{port}" if port != 3389 else host + lines = [ + "screen mode id:i:2", + "use multimon:i:0", + "desktopwidth:i:1920", + "desktopheight:i:1080", + "session bpp:i:32", + "compression:i:1", + "keyboardhook:i:2", + "audiocapturemode:i:0", + "videoplaybackmode:i:1", + "connection type:i:7", + "networkautodetect:i:1", + "bandwidthautodetect:i:1", + "displayconnectionbar:i:1", + "bitmapcachepersistenable:i:1", + f"full address:s:{full}", + "audiomode:i:0", + "redirectclipboard:i:1", + "autoreconnection enabled:i:1", + "authentication level:i:2", + "prompt for credentials:i:1", + "negotiate security layer:i:1", + "gatewayusagemethod:i:4", + "gatewaycredentialssource:i:4", + "gatewayprofileusagemethod:i:0", + ] + if username: + lines.insert(15, f"username:s:{username}") + body = "\r\n".join(lines) + "\r\n" + safe = "".join(c if c.isalnum() or c in ".-_" else "_" for c in host)[:80] or "session" + return Response( + content=body, + media_type="application/x-rdp", + headers={ + "Content-Disposition": f'attachment; filename="{safe}.rdp"', + "Cache-Control": "no-store", + }, + ) + + @app.get("/vendor/xterm/xterm.css") async def vendor_xterm_css(): return FileResponse(STATIC_DIR / "vendor/xterm/xterm.css", media_type="text/css") @@ -2277,3 +5133,18 @@ async def dell_mark(): @app.get("/dell.svg") async def dell_svg(): return FileResponse(STATIC_DIR / "dell.png", media_type="image/png") + + +@app.get("/assets/dell/{filename}") +async def dell_asset(filename: str): + """Serve Dell product face photos for rack Design Studio.""" + safe = Path(filename).name + if not safe or safe != filename or ".." in filename: + raise HTTPException(400, "invalid filename") + path = STATIC_DIR / "assets" / "dell" / safe + if not path.is_file(): + raise HTTPException(404, "asset not found") + media = "image/jpeg" if safe.lower().endswith((".jpg", ".jpeg")) else "image/png" + resp = FileResponse(path, media_type=media) + resp.headers["Cache-Control"] = "public, max-age=86400" + return resp diff --git a/ui/app.js b/ui/app.js index 53879f6..cdadebb 100644 --- a/ui/app.js +++ b/ui/app.js @@ -757,6 +757,7 @@
+ @@ -1480,7 +1481,13 @@ SSH terminal Login with your username · ${escapeHtml(ip)} ` : ""} + ${ip ? `` : ""}
`; + mount.appendChild(box); + $("#btn-open-hw")?.addEventListener("click", () => { + window.cockpitReports?.open?.(); + // slight delay for drawer open + setTimeout(() => { + if (window.cockpitReports) { + window.cockpitReports.state = window.cockpitReports.state || {}; + } + const st = document.querySelector('#reports-tabs [data-tab="hardware"]'); + st?.click(); + // set device after tab loads + setTimeout(() => { + const btn = document.querySelector(`[data-hw-id="${deviceId}"]`); + btn?.click(); + }, 400); + }, 200); + }); + } catch (_) { + /* ignore */ + } + } + + async function loadWarrantyCompliance(deviceId) { const mount = $("#warranty-compliance-mount"); if (!mount) return; try { @@ -1681,6 +1733,7 @@ ${node.idrac_url ? `iDRAC Web` : ""} ${node.ip ? `` : ""} + ${node.ip ? `` : ""} @@ -1715,8 +1768,10 @@ updateFocusContext(); $("#btn-quick-connect")?.addEventListener("click", () => openConnect(node)); $("#btn-ssh-term")?.addEventListener("click", () => window.cockpitSsh?.open(node)); + $("#btn-rdp-term")?.addEventListener("click", () => window.cockpitRdp?.open(node)); $("#btn-ask-ai")?.addEventListener("click", () => openAi(node)); loadWarrantyCompliance(node.id); + loadExpansionPreview(node.id); const cachedInv = state.inventoryCache[node.id]; if (cachedInv) { const mount = $("#inv-mount"); @@ -2061,6 +2116,8 @@ showToast("Focused " + (node.name || "")); } else if (a === "ssh") { window.cockpitSsh?.open(node); + } else if (a === "rdp") { + window.cockpitRdp?.open(node); } else if (a === "connect") { closeKpiPopup(); openConnect(node); @@ -2238,6 +2295,25 @@ } }); + function applyTheme(theme) { + const t = theme === "dark" ? "dark" : "light"; + document.documentElement.setAttribute("data-theme", t); + try { + localStorage.setItem("cockpit_theme", t); + } catch (_) {} + const btn = $("#btn-theme"); + if (btn) { + btn.dataset.theme = t; + btn.title = t === "light" ? "Switch to dark theme" : "Switch to light theme"; + btn.setAttribute("aria-label", btn.title); + } + } + applyTheme(localStorage.getItem("cockpit_theme") || "light"); + $("#btn-theme")?.addEventListener("click", () => { + const cur = document.documentElement.getAttribute("data-theme") || "light"; + applyTheme(cur === "light" ? "dark" : "light"); + }); + $("#btn-reset-view").addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); diff --git a/ui/assets/dell/face-blank-1u.jpg b/ui/assets/dell/face-blank-1u.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f02a4858e96ead7cfe17fac8c66d8bdb17224389 GIT binary patch literal 3692 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 literal 0 HcmV?d00001 diff --git a/ui/assets/dell/face-chassis-mx.jpg b/ui/assets/dell/face-chassis-mx.jpg new file mode 100644 index 0000000000000000000000000000000000000000..80534e794f7d12feeea27118a8bf9f472b9a3cbd GIT binary patch literal 23979 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ν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 literal 0 HcmV?d00001 diff --git a/ui/assets/dell/face-server-1u.jpg b/ui/assets/dell/face-server-1u.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c4f1fd8d2fd7f2a02d461746d3c2193d50162db9 GIT binary patch literal 28352 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

FzA4zr*e`;bTp+UhNQH27sz)#7FvpO^uk_k7a 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 literal 0 HcmV?d00001 diff --git a/ui/assets/dell/face-storage-2u.jpg b/ui/assets/dell/face-storage-2u.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7639a168fc884c63afef13c61ffcc155bb51f0d3 GIT binary patch literal 29443 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 z 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 - + +

@@ -28,9 +38,10 @@
+ - +
@@ -96,18 +107,6 @@ -
- -
-

Live atc-gpu-prod · llama3-70b-gptq

-
Waiting for GPU telemetry…
-
-
-
-
+ + + @@ -278,6 +296,30 @@ + + +