feat(topo): per-socket bar layout with disks under their controller
Rework the /topo diagram from side-by-side stacked cards into one tall vertical bar per CPU socket with everything attached to it branching off sideways (socket 0 left/branches right, socket 1 right/branches left). Disks are now parented under the storage controller they physically hang off (SATA/AHCI, SAS HBA, RAID) — itself a NUMA-affine PCIe device under one socket — instead of a synthetic catch-all node. The disk->controller link is read from a new storage-controllers.txt techdump (platform.StorageControllerMapScript, a /sys/block walk); disks with no resolvable controller fall back to an "Other" bar. No board/root node. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SYjrG6bVmeG1Z2Wmc8kg3o
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b1ab866f58
commit
319f08ac4b
@@ -152,6 +152,187 @@ func TestTopoPageRendersArbitraryPSUAndFirmwareCountsAsFlexRows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTopoPageRendersStorageDisksGroupedByType(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "audit.json")
|
||||
|
||||
okStatus := "OK"
|
||||
warnStatus := "WARNING"
|
||||
ssd := "SSD"
|
||||
nvme := "NVMe"
|
||||
model := "SAMSUNG MZ7L3960HCJR-00B7C"
|
||||
size := 960
|
||||
|
||||
ingest := schema.HardwareIngestRequest{
|
||||
CollectedAt: "2026-03-15T00:00:00Z",
|
||||
Hardware: schema.HardwareSnapshot{
|
||||
Storage: []schema.HardwareStorage{
|
||||
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Type: &ssd, Model: &model, SizeGB: &size},
|
||||
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Type: &ssd, Model: &model, SizeGB: &size},
|
||||
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &warnStatus}, Type: &nvme, SizeGB: &size},
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := json.Marshal(ingest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler := NewHandler(HandlerOptions{AuditPath: path})
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/topo", nil))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d", rec.Code)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
|
||||
if !strings.Contains(body, "SSD ×2") {
|
||||
t.Fatalf("topo page missing grouped SSD x2 card: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, "NVMe") {
|
||||
t.Fatalf("topo page missing NVMe disk card: %s", body)
|
||||
}
|
||||
// SSD card totals capacity across both disks.
|
||||
if !strings.Contains(body, "1.9 TB total") {
|
||||
t.Fatalf("topo page missing SSD total capacity: %s", body)
|
||||
}
|
||||
// The degraded NVMe disk must escalate that card's status.
|
||||
nvmeIdx := strings.Index(body, ">NVMe<")
|
||||
if nvmeIdx < 0 || !strings.Contains(body[nvmeIdx:nvmeIdx+400], "Warning") {
|
||||
t.Fatalf("topo page missing NVMe warning status: %s", body)
|
||||
}
|
||||
// With no storage-controller techdump, disks have no resolvable socket and
|
||||
// land under the "Other" bar rather than being glued to a CPU.
|
||||
if !strings.Contains(body, ">Other<") {
|
||||
t.Fatalf("topo page missing Other bar for unattached disks: %s", body)
|
||||
}
|
||||
clicks := strings.Count(body, `openComponentDetail('storage')`) + strings.Count(body, `openComponentDetail('storage')`)
|
||||
if clicks != 2 {
|
||||
t.Fatalf("expected one clickable card per disk-type group, got %d: %s", clicks, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStorageControllerMap(t *testing.T) {
|
||||
raw := "sda hctl=2:0:0:0 ctrl=0000:00:17.0\n" +
|
||||
"sdb hctl=3:0:0:0 ctrl=0000:00:17.0\n" +
|
||||
"nvme0n1 hctl= ctrl=0000:65:00.0\n"
|
||||
got := parseStorageControllerMap(raw)
|
||||
if got["2:0:0:0"] != "0000:00:17.0" || got["3:0:0:0"] != "0000:00:17.0" {
|
||||
t.Fatalf("SATA disks not mapped to controller: %#v", got)
|
||||
}
|
||||
if _, ok := got["nvme0n1"]; ok {
|
||||
t.Fatalf("NVMe line with empty hctl must be skipped: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTopoPageParentsDisksUnderTheirControllerSocket(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "audit.json")
|
||||
techdump := filepath.Join(dir, "techdump")
|
||||
if err := os.MkdirAll(techdump, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Both SSDs hang off the SATA controller at 0000:00:17.0, which is a
|
||||
// NUMA-node-0 PCIe device -> they must render under CPU 0, not "Other".
|
||||
if err := os.WriteFile(filepath.Join(techdump, "storage-controllers.txt"),
|
||||
[]byte("sda hctl=2:0:0:0 ctrl=0000:00:17.0\nsdb hctl=3:0:0:0 ctrl=0000:00:17.0\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
socket0, socket1 := 0, 1
|
||||
numa0 := 0
|
||||
okStatus := "OK"
|
||||
sataClass := "SATA controller"
|
||||
sataBDF := "0000:00:17.0"
|
||||
hctlA, hctlB := "2:0:0:0", "3:0:0:0"
|
||||
ssd := "SSD"
|
||||
size := 960
|
||||
|
||||
ingest := schema.HardwareIngestRequest{
|
||||
CollectedAt: "2026-03-15T00:00:00Z",
|
||||
Hardware: schema.HardwareSnapshot{
|
||||
CPUs: []schema.HardwareCPU{
|
||||
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Socket: &socket0},
|
||||
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Socket: &socket1},
|
||||
},
|
||||
PCIeDevices: []schema.HardwarePCIeDevice{
|
||||
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Slot: &sataBDF, DeviceClass: &sataClass, NUMANode: &numa0},
|
||||
},
|
||||
Storage: []schema.HardwareStorage{
|
||||
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Slot: &hctlA, Type: &ssd, SizeGB: &size},
|
||||
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Slot: &hctlB, Type: &ssd, SizeGB: &size},
|
||||
},
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(ingest)
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler := NewHandler(HandlerOptions{AuditPath: path, ExportDir: dir})
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/topo", nil))
|
||||
body := rec.Body.String()
|
||||
|
||||
if strings.Contains(body, ">Other<") {
|
||||
t.Fatalf("disks resolved to a socket — no Other bar expected: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, "SATA ctrl") {
|
||||
t.Fatalf("topo page missing the SATA controller branch node: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, "SSD ×2") {
|
||||
t.Fatalf("topo page missing SSD disk group under the controller: %s", body)
|
||||
}
|
||||
// controller branch must render before its SSD sub-node
|
||||
if strings.Index(body, "SATA ctrl") > strings.Index(body, "SSD ×2") {
|
||||
t.Fatalf("controller node should render before its disks: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTopoPageRendersOneBarPerSocketNoRoot(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "audit.json")
|
||||
|
||||
socket0, socket1 := 0, 1
|
||||
okStatus := "OK"
|
||||
board := "AS-4125GS-TNRT"
|
||||
vendor := "Supermicro"
|
||||
|
||||
ingest := schema.HardwareIngestRequest{
|
||||
CollectedAt: "2026-03-15T00:00:00Z",
|
||||
Hardware: schema.HardwareSnapshot{
|
||||
Board: schema.HardwareBoard{ProductName: &board, Manufacturer: &vendor},
|
||||
CPUs: []schema.HardwareCPU{
|
||||
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Socket: &socket0},
|
||||
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Socket: &socket1},
|
||||
},
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(ingest)
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler := NewHandler(HandlerOptions{AuditPath: path})
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/topo", nil))
|
||||
body := rec.Body.String()
|
||||
|
||||
if !strings.Contains(body, ">CPU 0<") || !strings.Contains(body, ">CPU 1<") {
|
||||
t.Fatalf("topo diagram missing a bar per CPU socket: %s", body)
|
||||
}
|
||||
// No board/root node in the diagram itself (board identity is the
|
||||
// Firmware row lower down).
|
||||
svg := body[strings.Index(body, "<svg"):]
|
||||
svg = svg[:strings.Index(svg, "</svg>")]
|
||||
if strings.Contains(svg, board) {
|
||||
t.Fatalf("board node must not render inside the topology diagram: %s", svg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTopoPageLinkedFromNav(t *testing.T) {
|
||||
handler := NewHandler(HandlerOptions{})
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
Reference in New Issue
Block a user