Replaces the flat export/ + techdump/ + system/ + systemd/ layout (two
same-named-but-different techdump/ dirs, disk reports duplicated between a
SAT run dir and techdump/) with one shared categorizeExportTree used by both
BuildSupportBundle and the blackbox USB mirror:
- export/{cpu,memory,storage,gpu,network,platform}/ — raw vendor-tool
output grouped by device type, plus export/reanimator.json ready to POST
to Reanimator's /ingest/hardware endpoint.
- status/ — computed diagnosis (component-status.json, runtime-health.*,
and metrics.db, previously missing from every bundle entirely).
- tasks/ — bee's own task-run bookkeeping (bee-sat/, bee-bench/, task
reports, service logs) where duplication with export/ is expected.
- livecd/{gui,host}/ — live-boot/kiosk-session-only diagnostics, kept out
of the hardware-facing tree.
Drops the redundant disk-report mirror write in RunStorageAcceptancePack
and the now-dead syncDirectoryTree/removeMissingPaths; adds a size+mtime
skip in copyPath so the blackbox mirror doesn't rewrite unchanged files
every cycle. README.md rewritten to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
194 lines
6.9 KiB
Go
194 lines
6.9 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 == "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":
|
|
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.
|
|
func categorizeExportTree(srcExportDir, destRoot string) 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 _, 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()
|
|
}
|