package app import ( "archive/tar" "compress/gzip" "fmt" "io" "os" "os/exec" "path/filepath" "sort" "strings" "time" ) var supportBundleServices = []string{ "bee-audit.service", "bee-web.service", "bee-network.service", "bee-nvidia.service", "bee-preflight.service", "bee-sshsetup.service", } var supportBundleCommands = []struct { name string cmd []string }{ {name: "system/uname.txt", cmd: []string{"uname", "-a"}}, {name: "system/lsmod.txt", cmd: []string{"lsmod"}}, {name: "system/lspci-nn.txt", cmd: []string{"lspci", "-nn"}}, {name: "system/ip-addr.txt", cmd: []string{"ip", "addr"}}, {name: "system/ip-route.txt", cmd: []string{"ip", "route"}}, {name: "system/mount.txt", cmd: []string{"mount"}}, {name: "system/df-h.txt", cmd: []string{"df", "-h"}}, {name: "system/dmesg-tail.txt", cmd: []string{"sh", "-c", "dmesg | tail -n 200"}}, } func BuildSupportBundle(exportDir string) (string, error) { exportDir = strings.TrimSpace(exportDir) if exportDir == "" { exportDir = DefaultExportDir } if err := os.MkdirAll(exportDir, 0755); err != nil { return "", err } if err := cleanupOldSupportBundles(os.TempDir()); err != nil { return "", err } host := sanitizeFilename(hostnameOr("unknown")) ts := time.Now().UTC().Format("20060102-150405") stageRoot := filepath.Join(os.TempDir(), fmt.Sprintf("bee-support-%s-%s", host, ts)) if err := os.MkdirAll(stageRoot, 0755); err != nil { return "", err } defer os.RemoveAll(stageRoot) if err := copyDirContents(exportDir, filepath.Join(stageRoot, "export")); err != nil { return "", err } if err := writeJournalDump(filepath.Join(stageRoot, "systemd", "combined.journal.log")); err != nil { return "", err } for _, svc := range supportBundleServices { if err := writeCommandOutput(filepath.Join(stageRoot, "systemd", svc+".status.txt"), []string{"systemctl", "status", svc, "--no-pager"}); err != nil { return "", err } if err := writeCommandOutput(filepath.Join(stageRoot, "systemd", svc+".journal.log"), []string{"journalctl", "--no-pager", "-u", svc}); err != nil { return "", err } } for _, item := range supportBundleCommands { if err := writeCommandOutput(filepath.Join(stageRoot, item.name), item.cmd); err != nil { return "", err } } if err := writeManifest(filepath.Join(stageRoot, "manifest.txt"), exportDir, stageRoot); err != nil { return "", err } archivePath := filepath.Join(os.TempDir(), fmt.Sprintf("bee-support-%s-%s.tar.gz", host, ts)) if err := createSupportTarGz(archivePath, stageRoot); err != nil { return "", err } return archivePath, nil } func cleanupOldSupportBundles(dir string) error { matches, err := filepath.Glob(filepath.Join(dir, "bee-support-*.tar.gz")) if err != nil { return err } type entry struct { path string mod time.Time } list := make([]entry, 0, len(matches)) for _, match := range matches { info, err := os.Stat(match) if err != nil { continue } if time.Since(info.ModTime()) > 24*time.Hour { _ = os.Remove(match) continue } list = append(list, entry{path: match, mod: info.ModTime()}) } sort.Slice(list, func(i, j int) bool { return list[i].mod.After(list[j].mod) }) if len(list) > 3 { for _, old := range list[3:] { _ = os.Remove(old.path) } } return nil } func writeJournalDump(dst string) error { args := []string{"--no-pager"} for _, svc := range supportBundleServices { args = append(args, "-u", svc) } raw, err := exec.Command("journalctl", args...).CombinedOutput() if len(raw) == 0 && err != nil { raw = []byte(err.Error() + "\n") } if len(raw) == 0 { raw = []byte("no journal output\n") } if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { return err } return os.WriteFile(dst, raw, 0644) } func writeCommandOutput(dst string, cmd []string) error { if len(cmd) == 0 { return nil } raw, err := exec.Command(cmd[0], cmd[1:]...).CombinedOutput() if len(raw) == 0 { if err != nil { raw = []byte(err.Error() + "\n") } else { raw = []byte("no output\n") } } if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { return err } return os.WriteFile(dst, raw, 0644) } func writeManifest(dst, exportDir, stageRoot string) error { if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { return err } var body strings.Builder fmt.Fprintf(&body, "bee_version=%s\n", buildVersion()) fmt.Fprintf(&body, "host=%s\n", hostnameOr("unknown")) fmt.Fprintf(&body, "generated_at_utc=%s\n", time.Now().UTC().Format(time.RFC3339)) fmt.Fprintf(&body, "export_dir=%s\n", exportDir) fmt.Fprintf(&body, "\nfiles:\n") var files []string if err := filepath.Walk(stageRoot, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return err } if filepath.Clean(path) == filepath.Clean(dst) { return nil } rel, err := filepath.Rel(stageRoot, path) if err != nil { return err } files = append(files, fmt.Sprintf("%s\t%d", rel, info.Size())) return nil }); err != nil { return err } sort.Strings(files) for _, line := range files { body.WriteString(line) body.WriteByte('\n') } return os.WriteFile(dst, []byte(body.String()), 0644) } func buildVersion() string { raw, err := exec.Command("bee", "version").CombinedOutput() if err != nil { return "unknown" } return strings.TrimSpace(string(raw)) } func copyDirContents(srcDir, dstDir string) error { entries, err := os.ReadDir(srcDir) if err != nil { if os.IsNotExist(err) { return nil } return err } for _, entry := range entries { src := filepath.Join(srcDir, entry.Name()) dst := filepath.Join(dstDir, entry.Name()) if err := copyPath(src, dst); err != nil { return err } } return nil } func copyPath(src, dst string) error { info, err := os.Stat(src) if err != nil { return err } if info.IsDir() { if err := os.MkdirAll(dst, info.Mode().Perm()); err != nil { return err } entries, err := os.ReadDir(src) if err != nil { return err } for _, entry := range entries { if err := copyPath(filepath.Join(src, entry.Name()), filepath.Join(dst, entry.Name())); err != nil { return err } } return nil } if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { return err } in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode().Perm()) if err != nil { return err } defer out.Close() _, err = io.Copy(out, in) return err } func createSupportTarGz(dst, srcDir string) error { file, err := os.Create(dst) if err != nil { return err } defer file.Close() gz := gzip.NewWriter(file) defer gz.Close() tw := tar.NewWriter(gz) defer tw.Close() base := filepath.Dir(srcDir) return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } header, err := tar.FileInfoHeader(info, "") if err != nil { return err } header.Name, err = filepath.Rel(base, path) if err != nil { return err } if err := tw.WriteHeader(header); err != nil { return err } f, err := os.Open(path) if err != nil { return err } defer f.Close() _, err = io.Copy(tw, f) return err }) }