Files
bee/audit/internal/app/bundle_layout.go
T
mchusandClaude Sonnet 5 8a91f0f783 fix(webui): repair broken scenario Run button onclick, dedupe build.sh overlay staging
- page_scenario.go: onclick built via JSON.stringify() embedded raw double
  quotes inside a double-quoted HTML attribute, truncating the attribute so
  the click handler never compiled; pass the name through an escaped
  data-scenario-name attribute instead.
- build.sh: overlay staging rsyncs (OVERLAY_DIR->stage, stage->includes.chroot)
  ran without --delete, so a scenario removed from the repo (a9924b0) stayed
  baked into every ISO built from the persistent stage cache since — the
  "second script" in the Scenario page's list.
- blackbox: rewritten around a deterministic local zip + incremental
  patch-the-changed-suffix onto removable media, instead of walking/copying
  ~90 files through a synchronous ntfs-3g FUSE mount every cycle. journalctl
  captures are now "--since last sync" (were "--since boot", growing with
  uptime) and metrics.db is excluded (was copied whole every cycle).
- scenario: nvbandwidth-acs-ab now escalates GPU count (same-socket pair,
  other socket's pair, one cross-socket pair, all GPUs) under each ACS state
  instead of always running all 6 GPUs at once, using a new `bee
  gpu-bandwidth-groups` subcommand that discovers socket layout from
  `nvidia-smi topo -m` at runtime — gpu_indices is host-specific, so this
  can't be baked into the scenario file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 18:11:24 +03:00

199 lines
7.2 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. 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()
}