feat: add support bundle and raw audit export

This commit is contained in:
Mikhail Chusavitin
2026-03-16 18:20:26 +03:00
parent d18cde19c1
commit b25a2f6d30
37 changed files with 1699 additions and 97 deletions

View File

@@ -78,6 +78,13 @@ func (m model) updateConfirm(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
result, err := m.app.ExportLatestAuditResult(target)
return resultMsg{title: result.Title, body: result.Body, err: err, back: screenMain}
}
case actionExportBundle:
m.busyTitle = "Export support bundle"
target := *m.selectedTarget
return m, func() tea.Msg {
result, err := m.app.ExportSupportBundleResult(target)
return resultMsg{title: result.Title, body: result.Body, err: err, back: screenMain}
}
case actionRunNvidiaSAT:
m.busyTitle = "NVIDIA SAT"
return m, func() tea.Msg {
@@ -107,6 +114,8 @@ func (m model) confirmCancelTarget() screen {
switch m.pendingAction {
case actionExportAudit:
return screenExportTargets
case actionExportBundle:
return screenExportTargets
case actionRunNvidiaSAT:
fallthrough
case actionRunMemorySAT:

View File

@@ -4,11 +4,17 @@ import tea "github.com/charmbracelet/bubbletea"
func (m model) handleExportTargetsMenu() (tea.Model, tea.Cmd) {
if len(m.targets) == 0 {
return m, resultCmd("Export audit", "No removable filesystems found", nil, screenMain)
title := "Export audit"
if m.pendingAction == actionExportBundle {
title = "Export support bundle"
}
return m, resultCmd(title, "No removable filesystems found", nil, screenMain)
}
target := m.targets[m.cursor]
m.selectedTarget = &target
m.pendingAction = actionExportAudit
if m.pendingAction == actionNone {
m.pendingAction = actionExportAudit
}
m.screen = screenConfirm
return m, nil
}

View File

@@ -29,34 +29,57 @@ func (m model) handleMainMenu() (tea.Model, tea.Cmd) {
return resultMsg{title: result.Title, body: result.Body, err: err, back: screenMain}
}
case 4:
m.busy = true
m.busyTitle = "Run self-check"
return m, func() tea.Msg {
result, err := m.app.RunRuntimePreflightResult()
return resultMsg{title: result.Title, body: result.Body, err: err, back: screenMain}
}
case 5:
m.pendingAction = actionExportAudit
m.busy = true
m.busyTitle = "Export audit"
return m, func() tea.Msg {
targets, err := m.app.ListRemovableTargets()
return exportTargetsMsg{targets: targets, err: err}
}
case 5:
case 6:
m.pendingAction = actionExportBundle
m.busy = true
m.busyTitle = "Export support bundle"
return m, func() tea.Msg {
targets, err := m.app.ListRemovableTargets()
return exportTargetsMsg{targets: targets, err: err}
}
case 7:
m.busy = true
m.busyTitle = "Required tools"
return m, func() tea.Msg {
result := m.app.ToolCheckResult([]string{"dmidecode", "smartctl", "nvme", "ipmitool", "lspci", "ethtool", "bee", "nvidia-smi", "bee-gpu-stress", "memtester", "dhclient", "lsblk", "mount"})
return resultMsg{title: result.Title, body: result.Body, back: screenMain}
}
case 6:
case 8:
m.busy = true
m.busyTitle = "Health summary"
return m, func() tea.Msg {
result := m.app.HealthSummaryResult()
return resultMsg{title: result.Title, body: result.Body, back: screenMain}
}
case 7:
case 9:
m.busy = true
m.busyTitle = "Runtime issues"
return m, func() tea.Msg {
result := m.app.RuntimeHealthResult()
return resultMsg{title: result.Title, body: result.Body, back: screenMain}
}
case 10:
m.busy = true
m.busyTitle = "Audit logs"
return m, func() tea.Msg {
result := m.app.AuditLogTailResult()
return resultMsg{title: result.Title, body: result.Body, back: screenMain}
}
case 8:
case 11:
return m, tea.Quit
}
return m, nil

View File

@@ -454,13 +454,13 @@ func TestViewOutputScreenRendersBodyAndBackHint(t *testing.T) {
m := newTestModel()
m.screen = screenOutput
m.title = "Run audit"
m.body = "audit output: /var/log/bee-audit.json\n"
m.body = "audit output: /appdata/bee/export/bee-audit.json\n"
view := m.View()
for _, want := range []string{
"Run audit",
"audit output: /var/log/bee-audit.json",
"audit output: /appdata/bee/export/bee-audit.json",
"[enter/esc] back [ctrl+c] quit",
} {
if !strings.Contains(view, want) {

View File

@@ -31,6 +31,7 @@ const (
actionDHCPOne actionKind = "dhcp_one"
actionStaticIPv4 actionKind = "static_ipv4"
actionExportAudit actionKind = "export_audit"
actionExportBundle actionKind = "export_bundle"
actionRunNvidiaSAT actionKind = "run_nvidia_sat"
actionRunMemorySAT actionKind = "run_memory_sat"
actionRunStorageSAT actionKind = "run_storage_sat"
@@ -89,9 +90,12 @@ func newModel(application *app.App, runtimeMode runtimeenv.Mode) model {
"Services",
"Acceptance tests",
"Run audit",
"Run self-check",
"Export audit",
"Export support bundle",
"Check tools",
"Show health summary",
"Show runtime issues",
"Show audit logs",
"Exit",
},

View File

@@ -31,7 +31,11 @@ func (m model) View() string {
case screenAcceptance:
return renderMenu("Acceptance tests", "Select action", []string{"Run NVIDIA command pack", "Run memory test", "Run storage diagnostic pack", "Back"}, m.cursor)
case screenExportTargets:
return renderMenu("Export audit", "Select removable filesystem", renderTargetItems(m.targets), m.cursor)
title := "Export audit"
if m.pendingAction == actionExportBundle {
title = "Export support bundle"
}
return renderMenu(title, "Select removable filesystem", renderTargetItems(m.targets), m.cursor)
case screenInterfacePick:
return renderMenu("Interfaces", "Select interface", renderInterfaceItems(m.interfaces), m.cursor)
case screenStaticForm:
@@ -53,6 +57,11 @@ func (m model) confirmBody() (string, string) {
return "Export audit", "No target selected"
}
return "Export audit", fmt.Sprintf("Copy latest audit JSON to %s?", m.selectedTarget.Device)
case actionExportBundle:
if m.selectedTarget == nil {
return "Export support bundle", "No target selected"
}
return "Export support bundle", fmt.Sprintf("Copy support bundle archive to %s?", m.selectedTarget.Device)
case actionRunNvidiaSAT:
return "NVIDIA SAT", "Run NVIDIA acceptance command pack?"
case actionRunMemorySAT: