platform/app: restructure support bundle/blackbox mirror into device-type export/, status/, tasks/, livecd/
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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e03267a72f
commit
fa618d5abb
@@ -0,0 +1,193 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user