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
201 lines
7.4 KiB
Go
201 lines
7.4 KiB
Go
package app
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// DefaultMetricsDBPath is the SQLite time-series metrics store, kept outside
|
|
// DefaultExportDir (unlike everything else this package copies into a
|
|
// support bundle / blackbox mirror) because it's a live, actively-written
|
|
// database rather than a point-in-time capture.
|
|
const DefaultMetricsDBPath = "/appdata/bee/metrics.db"
|
|
|
|
// techdumpBucketFor maps a filename produced by platform.CaptureTechnicalDump
|
|
// into export/techdump/ to its device-type bucket under export/ in the
|
|
// categorized bundle layout, so a vendor engineer can find "the GPU stuff"
|
|
// without knowing bee's internal file names.
|
|
func techdumpBucketFor(name string) string {
|
|
switch {
|
|
case name == "lscpu.txt", name == "dmidecode-type4.txt":
|
|
return "cpu"
|
|
case name == "dmidecode-type17.txt":
|
|
return "memory"
|
|
case name == "lsblk.json", name == "storage-controllers.txt", name == "storcli64-drives.json",
|
|
name == "storcli2-show-all.json",
|
|
strings.HasPrefix(name, "smartctl-"), strings.HasPrefix(name, "nvme-"):
|
|
return "storage"
|
|
case name == "nvidia-smi-q.txt", name == "nvidia-smi-query.csv", name == "nvidia-smi-conf-compute-q.txt",
|
|
name == "nvidia-smi-topo.txt", name == "nvidia-smi-nvlink-status.txt", name == "nvidia-smi-nvlink-errors.txt",
|
|
name == "rocm-smi.txt", name == "rocm-smi-showallinfo.txt",
|
|
name == "pcie-nvidia-link.txt", name == "pcie-nvidia-link-under-load.txt", name == "kernel-aer-nvidia.txt":
|
|
return "gpu"
|
|
default:
|
|
// dmidecode-type0/1/2, ipmitool-*, sensors.json, lspci-vmm/vvv, and
|
|
// anything CaptureTechnicalDump adds later that isn't claimed above.
|
|
return "platform"
|
|
}
|
|
}
|
|
|
|
// keepForBundleCopy excludes previously-built bundle archives that may be
|
|
// sitting in the export dir (defensive — BuildSupportBundle itself stages
|
|
// and writes its .tar.gz under os.TempDir(), not exportDir).
|
|
func keepForBundleCopy(rel string, _ os.FileInfo) bool {
|
|
cleanRel := filepath.ToSlash(strings.TrimPrefix(filepath.Clean(rel), "./"))
|
|
if cleanRel == "" {
|
|
return true
|
|
}
|
|
if strings.HasPrefix(cleanRel, "bee-sat/") && strings.HasSuffix(cleanRel, ".tar.gz") {
|
|
return false
|
|
}
|
|
if strings.HasPrefix(filepath.Base(cleanRel), "bee-support-") && strings.HasSuffix(cleanRel, ".tar.gz") {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// categorizeExportTree copies the live bee export directory into destRoot,
|
|
// reshaped into the bundle's canonical layout:
|
|
//
|
|
// - export/<device-bucket>/ — raw vendor-tool output (persisted techdump),
|
|
// vendor-neutral, grouped by what the hardware is rather than which bee
|
|
// tool produced it.
|
|
// - export/reanimator.json — the hardware snapshot, ready to POST to
|
|
// Reanimator's /ingest/hardware endpoint.
|
|
// - status/ — computed diagnosis, not raw data: component
|
|
// health verdicts, runtime status, metrics time series.
|
|
// - tasks/ — bee's own task-run bookkeeping (SAT/bench
|
|
// run logs, orchestration state, service logs). Duplication with
|
|
// export/ here is expected (e.g. a SAT run's narrative disk report).
|
|
//
|
|
// Used by both BuildSupportBundle (on-demand tar.gz) and the blackbox USB
|
|
// mirror, so both artifacts share one shape. includeMetricsDB is false for
|
|
// the blackbox mirror: metrics.db is a live, growing SQLite file and copying
|
|
// it whole every sync cycle was a major contributor to slow blackbox cycles
|
|
// (see blackbox_archive.go); the on-demand support bundle still wants it.
|
|
func categorizeExportTree(srcExportDir, destRoot string, includeMetricsDB bool) error {
|
|
exportDir := filepath.Join(destRoot, "export")
|
|
statusDir := filepath.Join(destRoot, "status")
|
|
tasksDir := filepath.Join(destRoot, "tasks")
|
|
stateDir := filepath.Join(tasksDir, "_state")
|
|
servicesDir := filepath.Join(tasksDir, "_services")
|
|
|
|
for _, dir := range []string{exportDir, statusDir, tasksDir, stateDir, servicesDir} {
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Persisted hardware captures -> export/<bucket>/
|
|
techdumpSrc := filepath.Join(srcExportDir, "techdump")
|
|
if entries, err := os.ReadDir(techdumpSrc); err == nil {
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
// Stale leftover from before RunStorageAcceptancePack stopped
|
|
// mirroring its narrative disk report here (it's bee's own
|
|
// verdict, not raw tool output — belongs in tasks/bee-sat/ only).
|
|
// Units captured with an older bee build may still have one on
|
|
// disk; don't propagate it into the categorized layout.
|
|
if strings.HasPrefix(entry.Name(), "disk-") && strings.HasSuffix(entry.Name(), "-report.txt") {
|
|
continue
|
|
}
|
|
dst := filepath.Join(exportDir, techdumpBucketFor(entry.Name()), entry.Name())
|
|
if err := copyPath(filepath.Join(techdumpSrc, entry.Name()), dst); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
// bee-audit.json -> tasks/_state/ (internal record) and
|
|
// export/reanimator.json (vendor-facing copy), both carrying the latest
|
|
// SAT-overlay-normalized status verdicts.
|
|
if data, err := os.ReadFile(filepath.Join(srcExportDir, "bee-audit.json")); err == nil {
|
|
normalized, err := ApplySATOverlay(data)
|
|
if err != nil {
|
|
normalized = data
|
|
}
|
|
if err := os.WriteFile(filepath.Join(stateDir, "bee-audit.json"), normalized, 0644); err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(filepath.Join(exportDir, "reanimator.json"), normalized, 0644); err != nil {
|
|
return err
|
|
}
|
|
} else if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
// Computed diagnosis data -> status/
|
|
for _, name := range []string{"component-status.json", "runtime-health.json", "runtime-health.log"} {
|
|
src := filepath.Join(srcExportDir, name)
|
|
if _, err := os.Stat(src); err != nil {
|
|
continue
|
|
}
|
|
if err := copyPath(src, filepath.Join(statusDir, name)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if includeMetricsDB {
|
|
if _, err := os.Stat(DefaultMetricsDBPath); err == nil {
|
|
if err := copyPath(DefaultMetricsDBPath, filepath.Join(statusDir, "metrics.db")); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remaining orchestration state -> tasks/_state/
|
|
for _, name := range []string{"blackbox-state.json", "tasks-state.json"} {
|
|
src := filepath.Join(srcExportDir, name)
|
|
if _, err := os.Stat(src); err != nil {
|
|
continue
|
|
}
|
|
if err := copyPath(src, filepath.Join(stateDir, name)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// bee-*.log service logs -> tasks/_services/
|
|
if entries, err := os.ReadDir(srcExportDir); err == nil {
|
|
for _, entry := range entries {
|
|
name := entry.Name()
|
|
if entry.IsDir() || !strings.HasPrefix(name, "bee-") || !strings.HasSuffix(name, ".log") {
|
|
continue
|
|
}
|
|
if err := copyPath(filepath.Join(srcExportDir, name), filepath.Join(servicesDir, name)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
return err
|
|
}
|
|
|
|
// SAT/bench run dirs, unchanged internally -> tasks/bee-sat/, tasks/bee-bench/
|
|
for _, name := range []string{"bee-sat", "bee-bench"} {
|
|
src := filepath.Join(srcExportDir, name)
|
|
if _, err := os.Stat(src); err != nil {
|
|
continue
|
|
}
|
|
if err := copyPathFiltered(srcExportDir, src, filepath.Join(tasksDir, name), keepForBundleCopy); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Orchestration task-run reports, unchanged internally -> tasks/<NNN>_..._done/
|
|
if src := filepath.Join(srcExportDir, "tasks"); dirExists(src) {
|
|
if err := copyDirContentsFiltered(src, tasksDir, keepForBundleCopy); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func dirExists(path string) bool {
|
|
info, err := os.Stat(path)
|
|
return err == nil && info.IsDir()
|
|
}
|