- Remove audit/internal/tui/ (~3000 LOC, bubbletea/lipgloss/reanimator deps) - Add /api/* REST+SSE endpoints: audit, SAT (nvidia/memory/storage/cpu), services, network, export, tools, live metrics stream - Add async job manager with SSE streaming for long-running operations - Add platform.SampleLiveMetrics() for live fan/temp/power/GPU polling - Add multi-page web UI (vanilla JS): Dashboard, Metrics charts, Tests, Burn-in, Network, Services, Export, Tools - Add bee-desktop.service: openbox + Xorg + Chromium opening http://localhost/ - Add openbox/tint2/xorg/xinit/xterm/chromium to ISO package list - Update .profile, bee.sh, and bible-local docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
370 lines
9.6 KiB
Go
370 lines
9.6 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
|
|
"bee/audit/internal/app"
|
|
"bee/audit/internal/platform"
|
|
"bee/audit/internal/runtimeenv"
|
|
"bee/audit/internal/webui"
|
|
)
|
|
|
|
var Version = "dev"
|
|
|
|
func main() {
|
|
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
|
|
}
|
|
|
|
func run(args []string, stdout, stderr io.Writer) int {
|
|
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
|
Level: slog.LevelInfo,
|
|
})))
|
|
|
|
if len(args) == 0 {
|
|
printRootUsage(stderr)
|
|
return 2
|
|
}
|
|
|
|
switch args[0] {
|
|
case "help", "--help", "-h":
|
|
if len(args) > 1 {
|
|
return runHelp(args[1:], stdout, stderr)
|
|
}
|
|
printRootUsage(stdout)
|
|
return 0
|
|
case "audit":
|
|
return runAudit(args[1:], stdout, stderr)
|
|
case "export":
|
|
return runExport(args[1:], stdout, stderr)
|
|
case "preflight":
|
|
return runPreflight(args[1:], stdout, stderr)
|
|
case "support-bundle":
|
|
return runSupportBundle(args[1:], stdout, stderr)
|
|
case "web":
|
|
return runWeb(args[1:], stdout, stderr)
|
|
case "sat":
|
|
return runSAT(args[1:], stdout, stderr)
|
|
case "version", "--version", "-version":
|
|
fmt.Fprintln(stdout, Version)
|
|
return 0
|
|
default:
|
|
fmt.Fprintf(stderr, "bee: unknown command %q\n\n", args[0])
|
|
printRootUsage(stderr)
|
|
return 2
|
|
}
|
|
}
|
|
|
|
func printRootUsage(w io.Writer) {
|
|
fmt.Fprintln(w, `bee commands:
|
|
bee audit --runtime auto|local|livecd --output stdout|file:<path>
|
|
bee preflight --output stdout|file:<path>
|
|
bee export --target <device>
|
|
bee support-bundle --output stdout|file:<path>
|
|
bee web --listen :80 --audit-path `+app.DefaultAuditJSONPath+`
|
|
bee sat nvidia|memory|storage|cpu [--duration <seconds>]
|
|
bee version
|
|
bee help [command]`)
|
|
}
|
|
|
|
func runHelp(args []string, stdout, stderr io.Writer) int {
|
|
switch args[0] {
|
|
case "audit":
|
|
return runAudit([]string{"--help"}, stdout, stdout)
|
|
case "export":
|
|
return runExport([]string{"--help"}, stdout, stdout)
|
|
case "preflight":
|
|
return runPreflight([]string{"--help"}, stdout, stdout)
|
|
case "support-bundle":
|
|
return runSupportBundle([]string{"--help"}, stdout, stdout)
|
|
case "web":
|
|
return runWeb([]string{"--help"}, stdout, stdout)
|
|
case "sat":
|
|
return runSAT([]string{"--help"}, stdout, stderr)
|
|
case "version":
|
|
fmt.Fprintln(stdout, "usage: bee version")
|
|
return 0
|
|
default:
|
|
fmt.Fprintf(stderr, "bee help: unknown command %q\n\n", args[0])
|
|
printRootUsage(stderr)
|
|
return 2
|
|
}
|
|
}
|
|
|
|
func runAudit(args []string, stdout, stderr io.Writer) int {
|
|
fs := flag.NewFlagSet("audit", flag.ContinueOnError)
|
|
fs.SetOutput(stderr)
|
|
output := fs.String("output", "stdout", "output destination: stdout or file:<path>")
|
|
runtimeFlag := fs.String("runtime", "auto", "runtime environment: auto, local, livecd")
|
|
showVersion := fs.Bool("version", false, "print version and exit")
|
|
fs.Usage = func() {
|
|
fmt.Fprintln(stderr, "usage: bee audit [--runtime auto|local|livecd] [--output stdout|file:<path>]")
|
|
fs.PrintDefaults()
|
|
}
|
|
if err := fs.Parse(args); err != nil {
|
|
if err == flag.ErrHelp {
|
|
return 0
|
|
}
|
|
return 2
|
|
}
|
|
if fs.NArg() != 0 {
|
|
fs.Usage()
|
|
return 2
|
|
}
|
|
if *showVersion {
|
|
fmt.Fprintln(stdout, Version)
|
|
return 0
|
|
}
|
|
|
|
runtimeInfo, err := runtimeenv.Detect(*runtimeFlag)
|
|
if err != nil {
|
|
slog.Error("resolve runtime", "err", err)
|
|
return 1
|
|
}
|
|
slog.Info("runtime resolved", "mode", runtimeInfo.Mode, "reason", runtimeInfo.Reason)
|
|
|
|
application := app.New(platform.New())
|
|
path, err := application.RunAudit(runtimeInfo.Mode, *output)
|
|
if err != nil {
|
|
slog.Error("run audit", "err", err)
|
|
return 1
|
|
}
|
|
if path != "stdout" {
|
|
slog.Info("audit output written", "path", path)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
|
|
func runExport(args []string, stdout, stderr io.Writer) int {
|
|
fs := flag.NewFlagSet("export", flag.ContinueOnError)
|
|
fs.SetOutput(stderr)
|
|
targetDevice := fs.String("target", "", "removable device path, e.g. /dev/sdb1")
|
|
fs.Usage = func() {
|
|
fmt.Fprintln(stderr, "usage: bee export --target <device>")
|
|
fs.PrintDefaults()
|
|
}
|
|
if err := fs.Parse(args); err != nil {
|
|
if err == flag.ErrHelp {
|
|
return 0
|
|
}
|
|
return 2
|
|
}
|
|
if fs.NArg() != 0 {
|
|
fs.Usage()
|
|
return 2
|
|
}
|
|
if strings.TrimSpace(*targetDevice) == "" {
|
|
fmt.Fprintln(stderr, "bee export: --target is required")
|
|
fs.Usage()
|
|
return 2
|
|
}
|
|
|
|
application := app.New(platform.New())
|
|
targets, err := application.ListRemovableTargets()
|
|
if err != nil {
|
|
slog.Error("list removable targets", "err", err)
|
|
return 1
|
|
}
|
|
|
|
for _, target := range targets {
|
|
if target.Device == *targetDevice {
|
|
path, err := application.ExportLatestAudit(target)
|
|
if err != nil {
|
|
slog.Error("export latest audit", "err", err)
|
|
return 1
|
|
}
|
|
slog.Info("audit exported", "path", path)
|
|
return 0
|
|
}
|
|
}
|
|
|
|
slog.Error("target device not found among removable filesystems", "device", *targetDevice)
|
|
return 1
|
|
}
|
|
|
|
func runPreflight(args []string, stdout, stderr io.Writer) int {
|
|
fs := flag.NewFlagSet("preflight", flag.ContinueOnError)
|
|
fs.SetOutput(stderr)
|
|
output := fs.String("output", "stdout", "output destination: stdout or file:<path>")
|
|
fs.Usage = func() {
|
|
fmt.Fprintf(stderr, "usage: bee preflight [--output stdout|file:%s]\n", app.DefaultRuntimeJSONPath)
|
|
fs.PrintDefaults()
|
|
}
|
|
if err := fs.Parse(args); err != nil {
|
|
if err == flag.ErrHelp {
|
|
return 0
|
|
}
|
|
return 2
|
|
}
|
|
if fs.NArg() != 0 {
|
|
fs.Usage()
|
|
return 2
|
|
}
|
|
application := app.New(platform.New())
|
|
path, err := application.RunRuntimePreflight(*output)
|
|
if err != nil {
|
|
slog.Error("run preflight", "err", err)
|
|
return 1
|
|
}
|
|
if path != "stdout" {
|
|
slog.Info("runtime health written", "path", path)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func runSupportBundle(args []string, stdout, stderr io.Writer) int {
|
|
fs := flag.NewFlagSet("support-bundle", flag.ContinueOnError)
|
|
fs.SetOutput(stderr)
|
|
output := fs.String("output", "stdout", "output destination: stdout or file:<path>")
|
|
fs.Usage = func() {
|
|
fmt.Fprintln(stderr, "usage: bee support-bundle [--output stdout|file:<path>]")
|
|
fs.PrintDefaults()
|
|
}
|
|
if err := fs.Parse(args); err != nil {
|
|
if err == flag.ErrHelp {
|
|
return 0
|
|
}
|
|
return 2
|
|
}
|
|
if fs.NArg() != 0 {
|
|
fs.Usage()
|
|
return 2
|
|
}
|
|
path, err := app.BuildSupportBundle(app.DefaultExportDir)
|
|
if err != nil {
|
|
slog.Error("build support bundle", "err", err)
|
|
return 1
|
|
}
|
|
defer os.Remove(path)
|
|
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
slog.Error("read support bundle", "err", err)
|
|
return 1
|
|
}
|
|
switch {
|
|
case *output == "stdout":
|
|
if _, err := stdout.Write(raw); err != nil {
|
|
slog.Error("write support bundle stdout", "err", err)
|
|
return 1
|
|
}
|
|
case strings.HasPrefix(*output, "file:"):
|
|
dst := strings.TrimPrefix(*output, "file:")
|
|
if err := os.WriteFile(dst, raw, 0644); err != nil {
|
|
slog.Error("write support bundle", "err", err)
|
|
return 1
|
|
}
|
|
slog.Info("support bundle written", "path", dst)
|
|
default:
|
|
fmt.Fprintln(stderr, "bee support-bundle: unknown output destination")
|
|
fs.Usage()
|
|
return 2
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func runWeb(args []string, stdout, stderr io.Writer) int {
|
|
fs := flag.NewFlagSet("web", flag.ContinueOnError)
|
|
fs.SetOutput(stderr)
|
|
listenAddr := fs.String("listen", ":8080", "listen address, e.g. :80")
|
|
auditPath := fs.String("audit-path", app.DefaultAuditJSONPath, "path to the latest audit JSON snapshot")
|
|
exportDir := fs.String("export-dir", app.DefaultExportDir, "directory with logs, SAT results, and support bundles")
|
|
title := fs.String("title", "Bee Hardware Audit", "page title")
|
|
fs.Usage = func() {
|
|
fmt.Fprintf(stderr, "usage: bee web [--listen :80] [--audit-path %s] [--export-dir %s] [--title \"Bee Hardware Audit\"]\n", app.DefaultAuditJSONPath, app.DefaultExportDir)
|
|
fs.PrintDefaults()
|
|
}
|
|
if err := fs.Parse(args); err != nil {
|
|
if err == flag.ErrHelp {
|
|
return 0
|
|
}
|
|
return 2
|
|
}
|
|
if fs.NArg() != 0 {
|
|
fs.Usage()
|
|
return 2
|
|
}
|
|
|
|
slog.Info("starting bee web", "listen", *listenAddr, "audit_path", *auditPath)
|
|
|
|
runtimeInfo, err := runtimeenv.Detect("auto")
|
|
if err != nil {
|
|
slog.Warn("resolve runtime for web", "err", err)
|
|
}
|
|
|
|
if err := webui.ListenAndServe(*listenAddr, webui.HandlerOptions{
|
|
Title: *title,
|
|
AuditPath: *auditPath,
|
|
ExportDir: *exportDir,
|
|
App: app.New(platform.New()),
|
|
RuntimeMode: runtimeInfo.Mode,
|
|
}); err != nil {
|
|
slog.Error("run web", "err", err)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func runSAT(args []string, stdout, stderr io.Writer) int {
|
|
if len(args) == 0 {
|
|
fmt.Fprintln(stderr, "usage: bee sat nvidia|memory|storage|cpu [--duration <seconds>]")
|
|
return 2
|
|
}
|
|
if args[0] == "help" || args[0] == "--help" || args[0] == "-h" {
|
|
fmt.Fprintln(stdout, "usage: bee sat nvidia|memory|storage|cpu [--duration <seconds>]")
|
|
return 0
|
|
}
|
|
|
|
fs := flag.NewFlagSet("sat", flag.ContinueOnError)
|
|
fs.SetOutput(stderr)
|
|
duration := fs.Int("duration", 0, "stress-ng duration in seconds (cpu only; default: 60)")
|
|
if err := fs.Parse(args[1:]); err != nil {
|
|
if err == flag.ErrHelp {
|
|
return 0
|
|
}
|
|
return 2
|
|
}
|
|
if fs.NArg() != 0 {
|
|
fmt.Fprintf(stderr, "bee sat: unexpected arguments\n")
|
|
return 2
|
|
}
|
|
|
|
target := args[0]
|
|
if target != "nvidia" && target != "memory" && target != "storage" && target != "cpu" {
|
|
fmt.Fprintf(stderr, "bee sat: unknown target %q\n", target)
|
|
fmt.Fprintln(stderr, "usage: bee sat nvidia|memory|storage|cpu [--duration <seconds>]")
|
|
return 2
|
|
}
|
|
|
|
application := app.New(platform.New())
|
|
var (
|
|
archive string
|
|
err error
|
|
)
|
|
switch target {
|
|
case "nvidia":
|
|
archive, err = application.RunNvidiaAcceptancePack("")
|
|
case "memory":
|
|
archive, err = application.RunMemoryAcceptancePack("")
|
|
case "storage":
|
|
archive, err = application.RunStorageAcceptancePack("")
|
|
case "cpu":
|
|
dur := *duration
|
|
if dur <= 0 {
|
|
dur = 60
|
|
}
|
|
archive, err = application.RunCPUAcceptancePack("", dur)
|
|
}
|
|
if err != nil {
|
|
slog.Error("run sat", "target", target, "err", err)
|
|
return 1
|
|
}
|
|
slog.Info("sat archive written", "target", target, "path", archive)
|
|
return 0
|
|
}
|