Files
bee/audit/internal/app/bundle_layout.go
T
Mikhail ChusavitinandClaude Sonnet 5 ba250330d5 fix(app): capture PCIe link / AER diagnostics at boot, not just on demand
pcie-nvidia-link.txt, pcie-nvidia-link-under-load.txt, and
kernel-aer-nvidia.txt only ever lived in supportBundleCommands, which
exclusively runs inside the on-demand "Download Support Bundle" web UI
action. The blackbox USB auto-sync worker never calls that function -
it only mirrors whatever CaptureTechnicalDump already wrote into the
live export tree at boot. So these three files were structurally
unreachable from a blackbox pull no matter how fresh the build was;
earlier analysis of a real blackbox misattributed their absence to
build/version drift instead.

Move the underlying scripts into shared exported constants
(platform.PCIeNvidiaLinkScript, PCIeNvidiaLinkUnderLoadScript,
KernelAERNvidiaScript) and add them to techDumpNvidiaCommands, so
CaptureTechnicalDump captures all three once at boot (bee-audit is
oneshot, so the ~8s bee-gpu-burn cost for the under-load sample is a
one-time boot cost, not a per-sync-cycle one) alongside the existing
nvidia-smi-* dumps. supportBundleCommands still re-runs the same
scripts on demand for a fresher sample - that's intentional, not a
duplicate to clean up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 18:39:28 +03:00

200 lines
7.3 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",
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()
}