package app import ( "encoding/json" "fmt" "os" "path/filepath" "strconv" "strings" "time" "bee/audit/internal/collector" "bee/audit/internal/platform" "bee/audit/internal/runtimeenv" ) const ( DefaultAuditJSONPath = "/var/log/bee-audit.json" DefaultAuditLogPath = "/var/log/bee-audit.log" ) type App struct { network networkManager services serviceManager exports exportManager tools toolManager sat satRunner } type ActionResult struct { Title string Body string } type networkManager interface { ListInterfaces() ([]platform.InterfaceInfo, error) DefaultRoute() string DHCPOne(iface string) (string, error) DHCPAll() (string, error) SetStaticIPv4(cfg platform.StaticIPv4Config) (string, error) } type serviceManager interface { ListBeeServices() ([]string, error) ServiceStatus(name string) (string, error) ServiceDo(name string, action platform.ServiceAction) (string, error) } type exportManager interface { ListRemovableTargets() ([]platform.RemovableTarget, error) ExportFileToTarget(src string, target platform.RemovableTarget) (string, error) } type toolManager interface { TailFile(path string, lines int) string CheckTools(names []string) []platform.ToolStatus } type satRunner interface { RunNvidiaAcceptancePack(baseDir string) (string, error) } func New(platform *platform.System) *App { return &App{ network: platform, services: platform, exports: platform, tools: platform, sat: platform, } } func (a *App) RunAudit(runtimeMode runtimeenv.Mode, output string) (string, error) { result := collector.Run(runtimeMode) data, err := json.MarshalIndent(result, "", " ") if err != nil { return "", err } switch { case output == "stdout": _, err := os.Stdout.Write(append(data, '\n')) return "stdout", err case strings.HasPrefix(output, "file:"): path := strings.TrimPrefix(output, "file:") if err := os.WriteFile(path, append(data, '\n'), 0644); err != nil { return "", err } return path, nil default: return "", fmt.Errorf("unknown output destination %q — use stdout or file:", output) } } func (a *App) RunAuditNow(runtimeMode runtimeenv.Mode) (ActionResult, error) { path, err := a.RunAudit(runtimeMode, "file:"+DefaultAuditJSONPath) body := "Audit completed." if path != "" { body = "Audit output: " + path } return ActionResult{Title: "Run audit", Body: body}, err } func (a *App) RunAuditToDefaultFile(runtimeMode runtimeenv.Mode) (string, error) { return a.RunAudit(runtimeMode, "file:"+DefaultAuditJSONPath) } func (a *App) ExportLatestAudit(target platform.RemovableTarget) (string, error) { if _, err := os.Stat(DefaultAuditJSONPath); err != nil { return "", err } filename := fmt.Sprintf("audit-%s-%s.json", sanitizeFilename(hostnameOr("unknown")), time.Now().UTC().Format("20060102-150405")) tmpPath := filepath.Join(os.TempDir(), filename) data, err := os.ReadFile(DefaultAuditJSONPath) if err != nil { return "", err } if err := os.WriteFile(tmpPath, data, 0644); err != nil { return "", err } defer os.Remove(tmpPath) return a.exports.ExportFileToTarget(tmpPath, target) } func (a *App) ExportLatestAuditResult(target platform.RemovableTarget) (ActionResult, error) { path, err := a.ExportLatestAudit(target) return ActionResult{Title: "Export audit", Body: "Audit exported to " + path}, err } func (a *App) ListInterfaces() ([]platform.InterfaceInfo, error) { return a.network.ListInterfaces() } func (a *App) DefaultRoute() string { return a.network.DefaultRoute() } func (a *App) DHCPOne(iface string) (string, error) { return a.network.DHCPOne(iface) } func (a *App) DHCPOneResult(iface string) (ActionResult, error) { body, err := a.network.DHCPOne(iface) return ActionResult{Title: "DHCP on " + iface, Body: body}, err } func (a *App) DHCPAll() (string, error) { return a.network.DHCPAll() } func (a *App) DHCPAllResult() (ActionResult, error) { body, err := a.network.DHCPAll() return ActionResult{Title: "DHCP all interfaces", Body: body}, err } func (a *App) SetStaticIPv4(cfg platform.StaticIPv4Config) (string, error) { return a.network.SetStaticIPv4(cfg) } func (a *App) SetStaticIPv4Result(cfg platform.StaticIPv4Config) (ActionResult, error) { body, err := a.network.SetStaticIPv4(cfg) return ActionResult{Title: "Static IPv4 on " + cfg.Interface, Body: body}, err } func (a *App) NetworkStatus() (ActionResult, error) { ifaces, err := a.network.ListInterfaces() if err != nil { return ActionResult{Title: "Network status"}, err } var body strings.Builder for _, iface := range ifaces { ipv4 := "(no IPv4)" if len(iface.IPv4) > 0 { ipv4 = strings.Join(iface.IPv4, ", ") } fmt.Fprintf(&body, "- %s: state=%s ip=%s\n", iface.Name, iface.State, ipv4) } if gw := a.network.DefaultRoute(); gw != "" { fmt.Fprintf(&body, "\nDefault route: %s\n", gw) } return ActionResult{Title: "Network status", Body: strings.TrimSpace(body.String())}, nil } func (a *App) DefaultStaticIPv4FormFields(iface string) []string { return []string{ "", "24", strings.TrimSpace(a.network.DefaultRoute()), "77.88.8.8 77.88.8.1 1.1.1.1 8.8.8.8", } } func (a *App) ParseStaticIPv4Config(iface string, fields []string) platform.StaticIPv4Config { get := func(index int) string { if index >= 0 && index < len(fields) { return strings.TrimSpace(fields[index]) } return "" } return platform.StaticIPv4Config{ Interface: iface, Address: get(0), Prefix: get(1), Gateway: get(2), DNS: strings.Fields(get(3)), } } func (a *App) ListBeeServices() ([]string, error) { return a.services.ListBeeServices() } func (a *App) ServiceStatus(name string) (string, error) { return a.services.ServiceStatus(name) } func (a *App) ServiceStatusResult(name string) (ActionResult, error) { body, err := a.services.ServiceStatus(name) return ActionResult{Title: "service: " + name, Body: body}, err } func (a *App) ServiceDo(name string, action platform.ServiceAction) (string, error) { return a.services.ServiceDo(name, action) } func (a *App) ServiceActionResult(name string, action platform.ServiceAction) (ActionResult, error) { body, err := a.services.ServiceDo(name, action) return ActionResult{Title: "service: " + name, Body: body}, err } func (a *App) ListRemovableTargets() ([]platform.RemovableTarget, error) { return a.exports.ListRemovableTargets() } func (a *App) TailFile(path string, lines int) string { return a.tools.TailFile(path, lines) } func (a *App) CheckTools(names []string) []platform.ToolStatus { return a.tools.CheckTools(names) } func (a *App) ToolCheckResult(names []string) ActionResult { var body strings.Builder for _, tool := range a.tools.CheckTools(names) { status := "MISSING" if tool.OK { status = "OK (" + tool.Path + ")" } fmt.Fprintf(&body, "- %s: %s\n", tool.Name, status) } return ActionResult{Title: "Required tools", Body: strings.TrimSpace(body.String())} } func (a *App) AuditLogTailResult() ActionResult { body := a.tools.TailFile(DefaultAuditLogPath, 40) + "\n\n" + a.tools.TailFile(DefaultAuditJSONPath, 20) return ActionResult{Title: "Audit log tail", Body: body} } func (a *App) RunNvidiaAcceptancePack(baseDir string) (string, error) { return a.sat.RunNvidiaAcceptancePack(baseDir) } func (a *App) RunNvidiaAcceptancePackResult(baseDir string) (ActionResult, error) { path, err := a.sat.RunNvidiaAcceptancePack(baseDir) return ActionResult{Title: "NVIDIA SAT", Body: "Archive written to " + path}, err } func (a *App) FormatToolStatuses(statuses []platform.ToolStatus) string { var body strings.Builder for _, tool := range statuses { status := "MISSING" if tool.OK { status = "OK (" + tool.Path + ")" } fmt.Fprintf(&body, "- %s: %s\n", tool.Name, status) } return strings.TrimSpace(body.String()) } func (a *App) ParsePrefix(raw string, fallback int) int { value, err := strconv.Atoi(strings.TrimSpace(raw)) if err != nil || value <= 0 { return fallback } return value } func hostnameOr(fallback string) string { hn, err := os.Hostname() if err != nil || strings.TrimSpace(hn) == "" { return fallback } return hn } func sanitizeFilename(v string) string { var out []rune for _, r := range v { switch { case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '_', r == '.': out = append(out, r) default: out = append(out, '-') } } if len(out) == 0 { return "unknown" } return string(out) }