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// — 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// 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/_..._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() }