fix: complete hardware collection diagnostics

This commit is contained in:
2026-08-25 17:04:33 +03:00
parent 33ace33de5
commit ccc781856e
19 changed files with 311 additions and 81 deletions
+5
View File
@@ -20,6 +20,7 @@ import (
) )
var Version = "dev" var Version = "dev"
var BuildCommit = "unknown"
func buildLabel() string { func buildLabel() string {
label := strings.TrimSpace(Version) label := strings.TrimSpace(Version)
@@ -92,6 +93,10 @@ func run(args []string, stdout, stderr io.Writer) (exitCode int) {
case "gpu-bandwidth-groups": case "gpu-bandwidth-groups":
return runGPUBandwidthGroups(args[1:], stdout, stderr) return runGPUBandwidthGroups(args[1:], stdout, stderr)
case "version", "--version", "-version": case "version", "--version", "-version":
if len(args) > 1 && args[1] == "--commit" {
fmt.Fprintln(stdout, BuildCommit)
return 0
}
fmt.Fprintln(stdout, Version) fmt.Fprintln(stdout, Version)
return 0 return 0
default: default:
@@ -232,6 +232,11 @@ func ApplySATResultToDB(db *ComponentStatusDB, target, archivePath string) {
if overall == "" { if overall == "" {
return return
} }
// An unsupported pack did not exercise a component. In particular, a
// non-GPU host must not acquire a fictitious "pcie:gpu:nvidia" record.
if overall == "UNSUPPORTED" {
return
}
source := "sat:" + target source := "sat:" + target
dbStatus := satStatusToDBStatus(overall) dbStatus := satStatusToDBStatus(overall)
+11
View File
@@ -642,6 +642,7 @@ func writeManifest(dst, exportDir, stageRoot string) error {
} }
var body strings.Builder var body strings.Builder
fmt.Fprintf(&body, "bee_version=%s\n", buildVersion()) fmt.Fprintf(&body, "bee_version=%s\n", buildVersion())
fmt.Fprintf(&body, "bee_git_commit=%s\n", buildCommit())
fmt.Fprintf(&body, "host=%s\n", hostnameOr("unknown")) 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, "generated_at_utc=%s\n", time.Now().UTC().Format(time.RFC3339))
fmt.Fprintf(&body, "export_dir=%s\n", exportDir) fmt.Fprintf(&body, "export_dir=%s\n", exportDir)
@@ -747,6 +748,16 @@ func buildVersion() string {
return strings.TrimSpace(string(raw)) return strings.TrimSpace(string(raw))
} }
func buildCommit() string {
ctx, cancel := context.WithTimeout(context.Background(), defaultCommandTimeout)
defer cancel()
raw, err := exec.CommandContext(ctx, "bee", "version", "--commit").CombinedOutput()
if err != nil || strings.TrimSpace(string(raw)) == "" {
return "unknown"
}
return strings.TrimSpace(string(raw))
}
func copyDirContents(srcDir, dstDir string) error { func copyDirContents(srcDir, dstDir string) error {
entries, err := os.ReadDir(srcDir) entries, err := os.ReadDir(srcDir)
if err != nil { if err != nil {
+17 -2
View File
@@ -34,6 +34,20 @@ var dmesgErrorPatterns = []*regexp.Regexp{
regexp.MustCompile(`(?i)\bdisabled\b`), regexp.MustCompile(`(?i)\bdisabled\b`),
} }
// Boot-time messages below are noisy configuration/driver diagnostics, not
// hardware incidents. Raw dmesg remains in the bundle, but presenting each
// of them as Critical makes the event log unusable.
var dmesgIgnorePatterns = []*regexp.Regexp{
regexp.MustCompile(`(?i)bridge window .* failed to assign`),
regexp.MustCompile(`(?i)^NVRM: loading NVIDIA .* Kernel Module`),
regexp.MustCompile(`(?i)^NVRM: Persistence mode is deprecated`),
regexp.MustCompile(`(?i)^nvidia: module verification failed:.*tainting kernel`),
regexp.MustCompile(`(?i)^Yama: disabled by default`),
regexp.MustCompile(`(?i)^ERST: .*initialized`),
regexp.MustCompile(`(?i)iommu sva bind failed: -95`),
regexp.MustCompile(`(?i)gpuClearFbhubPoisonIntrForBug`),
}
// collectDmesgErrors runs `dmesg -T` (or `dmesg` without -T on failure) and // collectDmesgErrors runs `dmesg -T` (or `dmesg` without -T on failure) and
// returns only lines that match known error/warning patterns. // returns only lines that match known error/warning patterns.
func collectDmesgErrors() []schema.HardwareEventLog { func collectDmesgErrors() []schema.HardwareEventLog {
@@ -77,6 +91,9 @@ func parseDmesgErrors(output string) []schema.HardwareEventLog {
if !matchesAny(message, dmesgErrorPatterns) { if !matchesAny(message, dmesgErrorPatterns) {
continue continue
} }
if matchesAny(message, dmesgIgnorePatterns) {
continue
}
severity := dmesgSeverity(message) severity := dmesgSeverity(message)
source := "dmesg" source := "dmesg"
@@ -114,10 +131,8 @@ func matchesAny(s string, patterns []*regexp.Regexp) bool {
// "fault" (de-fault), and "undead" would contain "dead". // "fault" (de-fault), and "undead" would contain "dead".
var dmesgSeverityCriticalPatterns = []*regexp.Regexp{ var dmesgSeverityCriticalPatterns = []*regexp.Regexp{
regexp.MustCompile(`(?i)\bpanic\b`), regexp.MustCompile(`(?i)\bpanic\b`),
regexp.MustCompile(`(?i)\baer\b`),
regexp.MustCompile(`(?i)\buncorrect`), regexp.MustCompile(`(?i)\buncorrect`),
regexp.MustCompile(`(?i)\bxid\b`), regexp.MustCompile(`(?i)\bxid\b`),
regexp.MustCompile(`(?i)\bnvrm\b`),
regexp.MustCompile(`(?i)\berror\b`), regexp.MustCompile(`(?i)\berror\b`),
regexp.MustCompile(`(?i)\bfault\b`), regexp.MustCompile(`(?i)\bfault\b`),
regexp.MustCompile(`(?i)\bfail(ed|ure)?\b`), regexp.MustCompile(`(?i)\bfail(ed|ure)?\b`),
@@ -38,11 +38,6 @@ func TestDmesgSeverity_xidCodes(t *testing.T) {
msg: "Yama: disabled by default; enable with sysctl kernel.yama.*", msg: "Yama: disabled by default; enable with sysctl kernel.yama.*",
want: statusWarning, want: statusWarning,
}, },
{
name: "benign NVRM driver load message still escalates via NVRM keyword",
msg: "NVRM: loading NVIDIA UNIX Open Kernel Module for x86_64 580.159.03",
want: statusCritical,
},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -54,3 +49,12 @@ func TestDmesgSeverity_xidCodes(t *testing.T) {
}) })
} }
} }
func TestParseDmesgErrorsSkipsKnownBootNoise(t *testing.T) {
entries := parseDmesgErrors("[Thu Aug 25 10:00:00 2026] pci 0000:00:02.0: bridge window [mem 0x0000-0x0000] to [bus 01-ff] failed to assign\n" +
"[Thu Aug 25 10:00:01 2026] NVRM: loading NVIDIA UNIX Open Kernel Module for x86_64\n" +
"[Thu Aug 25 10:00:02 2026] blk_update_request: I/O error, dev sda\n")
if len(entries) != 1 || entries[0].Severity == nil || *entries[0].Severity != statusCritical {
t.Fatalf("entries=%#v, want one critical real error", entries)
}
}
+47 -3
View File
@@ -6,6 +6,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings" "strings"
) )
@@ -42,6 +43,7 @@ func enrichPCIeWithNICTelemetry(devs []schema.HardwarePCIeDevice) []schema.Hardw
if len(ifaces) == 0 { if len(ifaces) == 0 {
continue continue
} }
sort.Strings(ifaces)
iface := ifaces[0] iface := ifaces[0]
devs[i].MacAddresses = collectInterfaceMACs(ifaces) devs[i].MacAddresses = collectInterfaceMACs(ifaces)
if devs[i].SerialNumber == nil { if devs[i].SerialNumber == nil {
@@ -58,11 +60,24 @@ func enrichPCIeWithNICTelemetry(devs []schema.HardwarePCIeDevice) []schema.Hardw
} }
} }
if out, err := ethtoolModuleQuery(iface); err == nil { for port, portIface := range ifaces {
if injectSFPDOMTelemetry(&devs[i], out) { out, err := ethtoolModuleQuery(portIface)
enriched++ if err != nil {
continue continue
} }
legacy := schema.HardwarePCIeDevice{}
if !injectSFPDOMTelemetry(&legacy, out) || legacy.SFPIdentifier == nil {
continue
}
devs[i].SFPModules = append(devs[i].SFPModules, sfpModuleFromLegacy(port, legacy))
// Keep the v2.10 scalar representation for old Reanimator installs.
if len(devs[i].SFPModules) == 1 {
copyLegacySFPFields(&devs[i], legacy)
}
}
if len(devs[i].SFPModules) > 0 {
enriched++
continue
} }
if len(devs[i].MacAddresses) > 0 || devs[i].Firmware != nil { if len(devs[i].MacAddresses) > 0 || devs[i].Firmware != nil {
enriched++ enriched++
@@ -72,6 +87,27 @@ func enrichPCIeWithNICTelemetry(devs []schema.HardwarePCIeDevice) []schema.Hardw
return devs return devs
} }
func sfpModuleFromLegacy(port int, dev schema.HardwarePCIeDevice) schema.HardwareSFPModule {
m := schema.HardwareSFPModule{Port: port, Identifier: dev.SFPIdentifier, Connector: dev.SFPConnector,
Vendor: dev.SFPVendor, PartNumber: dev.SFPPartNumber, SerialNumber: dev.SFPSerialNumber,
Revision: dev.SFPRevision, TransceiverType: dev.SFPTransceiverType,
TemperatureC: dev.SFPTemperatureC, TXPowerDBM: dev.SFPTXPowerDBM, RXPowerDBM: dev.SFPRXPowerDBM,
VoltageV: dev.SFPVoltageV, BiasMA: dev.SFPBiasMA}
if dev.SFPWavelengthNM != nil {
v := int(*dev.SFPWavelengthNM + 0.5)
m.WavelengthNM = &v
}
return m
}
func copyLegacySFPFields(dst *schema.HardwarePCIeDevice, src schema.HardwarePCIeDevice) {
dst.SFPPresent, dst.SFPIdentifier, dst.SFPConnector = src.SFPPresent, src.SFPIdentifier, src.SFPConnector
dst.SFPVendor, dst.SFPPartNumber, dst.SFPSerialNumber = src.SFPVendor, src.SFPPartNumber, src.SFPSerialNumber
dst.SFPRevision, dst.SFPTransceiverType = src.SFPRevision, src.SFPTransceiverType
dst.SFPWavelengthNM, dst.SFPTemperatureC, dst.SFPTXPowerDBM, dst.SFPRXPowerDBM = src.SFPWavelengthNM, src.SFPTemperatureC, src.SFPTXPowerDBM, src.SFPRXPowerDBM
dst.SFPVoltageV, dst.SFPBiasMA = src.SFPVoltageV, src.SFPBiasMA
}
func isNICDevice(dev schema.HardwarePCIeDevice) bool { func isNICDevice(dev schema.HardwarePCIeDevice) bool {
if dev.DeviceClass == nil { if dev.DeviceClass == nil {
return false return false
@@ -140,6 +176,14 @@ func injectSFPDOMTelemetry(dev *schema.HardwarePCIeDevice, raw string) bool {
s := strings.TrimSpace(val) s := strings.TrimSpace(val)
dev.SFPSerialNumber = &s dev.SFPSerialNumber = &s
changed = true changed = true
case key == "vendor rev":
s := strings.TrimSpace(val)
dev.SFPRevision = &s
changed = true
case key == "transceiver type":
s := strings.TrimSpace(val)
dev.SFPTransceiverType = &s
changed = true
case strings.Contains(key, "laser wavelength"): case strings.Contains(key, "laser wavelength"):
if f, ok := firstFloat(val); ok { if f, ok := firstFloat(val); ok {
dev.SFPWavelengthNM = &f dev.SFPWavelengthNM = &f
@@ -33,6 +33,14 @@ func TestParseSFPDOM(t *testing.T) {
} }
} }
func TestSFPModuleFromLegacyUsesContractShape(t *testing.T) {
identifier, wavelength := "QSFP-DD", 1310.4
m := sfpModuleFromLegacy(1, schema.HardwarePCIeDevice{SFPIdentifier: &identifier, SFPWavelengthNM: &wavelength})
if m.Port != 1 || m.Identifier == nil || *m.Identifier != "QSFP-DD" || m.WavelengthNM == nil || *m.WavelengthNM != 1310 {
t.Fatalf("module=%#v", m)
}
}
func TestParseLSPCIDetailSerial(t *testing.T) { func TestParseLSPCIDetailSerial(t *testing.T) {
raw := ` raw := `
05:00.0 Ethernet controller: Mellanox Technologies MT28908 Family [ConnectX-6] 05:00.0 Ethernet controller: Mellanox Technologies MT28908 Family [ConnectX-6]
+15 -2
View File
@@ -39,7 +39,7 @@ func collectPSUs(manufacturer string) []schema.HardwarePowerSupply {
if len(psus) == 0 { if len(psus) == 0 {
psus = synthesizePSUsFromSDR(sdrData) psus = synthesizePSUsFromSDR(sdrData)
} else { } else {
mergePSUSDR(psus, sdrData) psus = mergePSUSDR(psus, sdrData)
} }
} else if len(psus) == 0 { } else if len(psus) == 0 {
slog.Info("psu: ipmitool unavailable, skipping", "err", err) slog.Info("psu: ipmitool unavailable, skipping", "err", err)
@@ -248,6 +248,7 @@ var psuSlotPatterns = []*regexp.Regexp{
} }
// psuInputPowerKeywords matches AC-input power sensor names across vendors: // psuInputPowerKeywords matches AC-input power sensor names across vendors:
//
// MSI: PSU1_POWER_IN, PSU1_PIN // MSI: PSU1_POWER_IN, PSU1_PIN
// MLT: PSU1_PIN // MLT: PSU1_PIN
// xFusion: (matched via default fallback — no explicit keyword) // xFusion: (matched via default fallback — no explicit keyword)
@@ -262,6 +263,7 @@ func isPSUInputPower(name string) bool {
} }
// isPSUOutputPower matches DC-output power sensor names across vendors: // isPSUOutputPower matches DC-output power sensor names across vendors:
//
// MSI: PSU1_POWER_OUT // MSI: PSU1_POWER_OUT
// MLT: PSU1_POUT // MLT: PSU1_POUT
// xFusion: PS1 POut // xFusion: PS1 POut
@@ -410,12 +412,14 @@ func synthesizePSUsFromSDR(sdr map[int]psuSDR) []schema.HardwarePowerSupply {
return out return out
} }
func mergePSUSDR(psus []schema.HardwarePowerSupply, sdr map[int]psuSDR) { func mergePSUSDR(psus []schema.HardwarePowerSupply, sdr map[int]psuSDR) []schema.HardwarePowerSupply {
matched := map[int]bool{}
for i := range psus { for i := range psus {
slotIdx, err := strconv.Atoi(derefPSUSlot(psus[i].Slot)) slotIdx, err := strconv.Atoi(derefPSUSlot(psus[i].Slot))
if err != nil { if err != nil {
continue continue
} }
matched[slotIdx+1] = true
entry, ok := sdr[slotIdx+1] entry, ok := sdr[slotIdx+1]
if !ok { if !ok {
continue continue
@@ -450,6 +454,15 @@ func mergePSUSDR(psus []schema.HardwarePowerSupply, sdr map[int]psuSDR) {
} }
} }
} }
// FRU can be incomplete (notably on CX270); do not discard a PSU that is
// visible in SDR merely because another PSU was present in FRU.
missing := make(map[int]psuSDR)
for slot, entry := range sdr {
if !matched[slot] {
missing[slot] = entry
}
}
return append(psus, synthesizePSUsFromSDR(missing)...)
} }
func splitSDRFields(line string) []string { func splitSDRFields(line string) []string {
+16 -1
View File
@@ -1,6 +1,9 @@
package collector package collector
import "testing" import (
"bee/audit/internal/schema"
"testing"
)
func TestParsePSUSDR(t *testing.T) { func TestParsePSUSDR(t *testing.T) {
raw := ` raw := `
@@ -118,3 +121,15 @@ func TestSynthesizePSUsFromSDR(t *testing.T) {
t.Fatalf("life used=%v", got[0].LifeUsedPct) t.Fatalf("life used=%v", got[0].LifeUsedPct)
} }
} }
func TestMergePSUSDRAppendsPSUMissingFromFRU(t *testing.T) {
model := "PSU0"
slot := "0"
got := mergePSUSDR([]schema.HardwarePowerSupply{{Slot: &slot, Model: &model}}, map[int]psuSDR{
1: {slot: 1, status: statusOK},
2: {slot: 2, status: statusOK},
})
if len(got) != 2 || got[1].Slot == nil || *got[1].Slot != "1" {
t.Fatalf("PSUs=%#v, want FRU PSU0 plus synthesized PSU1", got)
}
}
+40 -5
View File
@@ -652,21 +652,56 @@ func appendUniqueStorage(base, extra []schema.HardwareStorage) []schema.Hardware
if len(extra) == 0 { if len(extra) == 0 {
return base return base
} }
seen := map[string]bool{} seen := map[string]int{}
for _, d := range base { for i, d := range base {
seen[storageIdentityKey(d)] = true if key := storageIdentityKey(d); key != "" {
seen[key] = i
}
} }
for _, d := range extra { for _, d := range extra {
key := storageIdentityKey(d) key := storageIdentityKey(d)
if key == "" || seen[key] { if key == "" {
continue
}
if idx, ok := seen[key]; ok {
mergeStorageRecord(&base[idx], d)
continue continue
} }
base = append(base, d) base = append(base, d)
seen[key] = true seen[key] = len(base) - 1
} }
return base return base
} }
func mergeStorageRecord(dst *schema.HardwareStorage, src schema.HardwareStorage) {
if dst.Model == nil {
dst.Model = src.Model
}
if dst.SizeGB == nil {
dst.SizeGB = src.SizeGB
}
if dst.Interface == nil {
dst.Interface = src.Interface
}
if dst.Firmware == nil {
dst.Firmware = src.Firmware
}
if dst.Slot == nil {
dst.Slot = src.Slot
}
if dst.Manufacturer == nil {
dst.Manufacturer = src.Manufacturer
}
if dst.Present == nil {
dst.Present = src.Present
}
// A RAID controller often reports JBOD as Unknown; never replace a direct
// device health reading with that weaker verdict.
if dst.Status == nil {
dst.Status = src.Status
}
}
func storageIdentityKey(d schema.HardwareStorage) string { func storageIdentityKey(d schema.HardwareStorage) string {
if d.SerialNumber != nil && strings.TrimSpace(*d.SerialNumber) != "" { if d.SerialNumber != nil && strings.TrimSpace(*d.SerialNumber) != "" {
return "sn:" + strings.ToLower(strings.TrimSpace(*d.SerialNumber)) return "sn:" + strings.ToLower(strings.TrimSpace(*d.SerialNumber))
@@ -6,6 +6,16 @@ import (
"testing" "testing"
) )
func TestAppendUniqueStorageMergesRAIDMetadataForSameDisk(t *testing.T) {
serial, status, model, iface := "SN-1", "OK", "Disk Model", "SAS"
base := []schema.HardwareStorage{{SerialNumber: &serial, HardwareComponentStatus: schema.HardwareComponentStatus{Status: &status}}}
extra := []schema.HardwareStorage{{SerialNumber: &serial, Model: &model, Interface: &iface}}
got := appendUniqueStorage(base, extra)
if len(got) != 1 || got[0].Model == nil || *got[0].Model != model || got[0].Interface == nil || *got[0].Interface != iface {
t.Fatalf("merged storage=%#v", got)
}
}
func TestParseSASIrcuControllerIDs(t *testing.T) { func TestParseSASIrcuControllerIDs(t *testing.T) {
raw := `LSI Corporation SAS2 IR Configuration Utility. raw := `LSI Corporation SAS2 IR Configuration Utility.
Adapter List Adapter List
@@ -43,6 +43,9 @@ func (s *System) RunNvidiaConfigCheckPack(ctx context.Context, baseDir string, l
if settings, err := s.ListNvidiaGPUSettings(); err != nil { if settings, err := s.ListNvidiaGPUSettings(); err != nil {
status.Notes = append(status.Notes, "nvidia-smi GPU settings unavailable (no driver, or no GPU present): "+err.Error()) status.Notes = append(status.Notes, "nvidia-smi GPU settings unavailable (no driver, or no GPU present): "+err.Error())
} else { } else {
if len(settings) == 0 {
status.NoGPUs = true
}
for _, g := range settings { for _, g := range settings {
f := NvidiaGPUConfigFinding{ f := NvidiaGPUConfigFinding{
Index: g.Index, Name: g.Name, Index: g.Index, Name: g.Name,
@@ -127,6 +130,7 @@ type NvidiaConfigCheckStatus struct {
GPUs []NvidiaGPUConfigFinding `json:"gpus,omitempty"` GPUs []NvidiaGPUConfigFinding `json:"gpus,omitempty"`
NVLinkPairs []NvidiaNVLinkPairFinding `json:"nvlink_pairs,omitempty"` NVLinkPairs []NvidiaNVLinkPairFinding `json:"nvlink_pairs,omitempty"`
NoGPUs bool `json:"no_gpus"`
// Confidential Computing readiness — informational only, does not gate // Confidential Computing readiness — informational only, does not gate
// overall_status: an unconfigured/NOT_READY CC state is a // overall_status: an unconfigured/NOT_READY CC state is a
@@ -466,7 +470,10 @@ func renderNvidiaConfigCheckSummary(status NvidiaConfigCheckStatus) string {
fmt.Fprintf(&b, "cc_state=%s\n", status.CCState) fmt.Fprintf(&b, "cc_state=%s\n", status.CCState)
fmt.Fprintf(&b, "cpu_cc_capability=%s\n", status.CPUCCCapability) fmt.Fprintf(&b, "cpu_cc_capability=%s\n", status.CPUCCCapability)
fmt.Fprintf(&b, "gpu_cc_capability=%s\n", status.GPUCCCapability) fmt.Fprintf(&b, "gpu_cc_capability=%s\n", status.GPUCCCapability)
if len(status.Warnings) == 0 { if status.NoGPUs {
fmt.Fprintln(&b, "overall_status=UNSUPPORTED")
fmt.Fprintln(&b, "reason=no_nvidia_gpus_detected")
} else if len(status.Warnings) == 0 {
fmt.Fprintln(&b, "overall_status=OK") fmt.Fprintln(&b, "overall_status=OK")
} else { } else {
fmt.Fprintln(&b, "overall_status=FAILED") fmt.Fprintln(&b, "overall_status=FAILED")
@@ -165,6 +165,10 @@ func TestRenderNvidiaConfigCheckSummaryOverallStatus(t *testing.T) {
if got := renderNvidiaConfigCheckSummary(withWarning); !strings.Contains(got, "overall_status=FAILED") { if got := renderNvidiaConfigCheckSummary(withWarning); !strings.Contains(got, "overall_status=FAILED") {
t.Fatalf("status with warnings missing overall_status=FAILED:\n%s", got) t.Fatalf("status with warnings missing overall_status=FAILED:\n%s", got)
} }
noGPU := NvidiaConfigCheckStatus{NoGPUs: true}
if got := renderNvidiaConfigCheckSummary(noGPU); !strings.Contains(got, "overall_status=UNSUPPORTED") {
t.Fatalf("no-GPU summary must be unsupported:\n%s", got)
}
} }
// TestRenderNvidiaConfigCheckSummaryIncludesWarningsField guards that a // TestRenderNvidiaConfigCheckSummaryIncludesWarningsField guards that a
+11
View File
@@ -920,6 +920,12 @@ func (s *System) RunStorageAcceptancePack(ctx context.Context, baseDir string, e
runSyncBracketHook(job, "after", logFunc) runSyncBracketHook(job, "after", logFunc)
} }
status, rc := classifySATResult(job.name, out, err) status, rc := classifySATResult(job.name, out, err)
// A zero smartctl exit status only proves the command ran. If the
// drive did not return its overall-health verdict, it must not turn
// the storage SAT green.
if job.name == "smartctl-health" && status == "OK" && !hasSMARTOverallHealth(out) {
status = "UNSUPPORTED"
}
stats.Add(status) stats.Add(status)
key := filepath.Base(devPath) + "_" + strings.ReplaceAll(job.name, "-", "_") key := filepath.Base(devPath) + "_" + strings.ReplaceAll(job.name, "-", "_")
fmt.Fprintf(&summary, "%s_rc=%d\n", key, rc) fmt.Fprintf(&summary, "%s_rc=%d\n", key, rc)
@@ -1560,6 +1566,11 @@ func classifySATResult(name string, out []byte, err error) (string, int) {
return "FAILED", rc return "FAILED", rc
} }
func hasSMARTOverallHealth(out []byte) bool {
m := smartHealthRE.FindStringSubmatch(string(out))
return len(m) > 1 && strings.TrimSpace(m[1]) != ""
}
func runSATCommand(verboseLog, name string, cmd []string, logFunc func(string)) ([]byte, error) { func runSATCommand(verboseLog, name string, cmd []string, logFunc func(string)) ([]byte, error) {
start := time.Now().UTC() start := time.Now().UTC()
resolvedCmd, err := resolveSATCommand(cmd) resolvedCmd, err := resolveSATCommand(cmd)
+10 -2
View File
@@ -124,6 +124,7 @@ func writeNVMeReport(b *strings.Builder, outputs map[string][]byte) {
writtenBytes: writtenBytes, writtenBytes: writtenBytes,
readBytes: readBytes, readBytes: readBytes,
capacityBytes: capacityBytes, capacityBytes: capacityBytes,
healthKnown: true,
} }
writeResourceSection(b, ri) writeResourceSection(b, ri)
@@ -232,6 +233,7 @@ func writeSATAReport(b *strings.Builder, outputs map[string][]byte) {
capacityBytes: capacityBytes, capacityBytes: capacityBytes,
readPercent: 100 - readValue, readPercent: 100 - readValue,
hasReadPercent: hasReadValue, hasReadPercent: hasReadValue,
healthKnown: !strings.EqualFold(health, "unknown"),
} }
writeResourceSection(b, ri) writeResourceSection(b, ri)
@@ -341,6 +343,7 @@ const (
type resourceInfo struct { type resourceInfo struct {
powerOnHours uint64 powerOnHours uint64
healthKnown bool
powerCycles uint64 powerCycles uint64
writtenBytes uint64 writtenBytes uint64
readBytes uint64 readBytes uint64
@@ -393,7 +396,10 @@ func writeConclusionSection(b *strings.Builder, r resourceInfo) {
writeSectionHeader(b, "Conclusion") writeSectionHeader(b, "Conclusion")
var reasons, notes []string var reasons, notes []string
isNew := true isNew := r.healthKnown
if !r.healthKnown {
notes = append(notes, "SMART overall health unavailable — disk cannot be accepted as NEW")
}
if r.capacityBytes > 0 { if r.capacityBytes > 0 {
writtenFrac := float64(r.writtenBytes) / float64(r.capacityBytes) writtenFrac := float64(r.writtenBytes) / float64(r.capacityBytes)
@@ -424,7 +430,9 @@ func writeConclusionSection(b *strings.Builder, r resourceInfo) {
reasons = append(reasons, fmt.Sprintf("power cycles %s", formatUint(r.powerCycles))) reasons = append(reasons, fmt.Sprintf("power cycles %s", formatUint(r.powerCycles)))
} }
if isNew { if !r.healthKnown {
writeField(b, "Disk Condition", "UNVERIFIED")
} else if isNew {
writeField(b, "Disk Condition", "NEW") writeField(b, "Disk Condition", "NEW")
} else { } else {
writeField(b, "Disk Condition", "USED") writeField(b, "Disk Condition", "USED")
@@ -141,6 +141,15 @@ func TestGenerateDiskReportSATA(t *testing.T) {
assertContains(t, report, "Power_On_Hours") assertContains(t, report, "Power_On_Hours")
} }
func TestHasSMARTOverallHealth(t *testing.T) {
if !hasSMARTOverallHealth([]byte("SMART overall-health self-assessment test result: PASSED\n")) {
t.Fatal("expected SMART health verdict to be recognized")
}
if hasSMARTOverallHealth([]byte("SMART support is: Available\n")) {
t.Fatal("availability alone must not be accepted as a health verdict")
}
}
func assertContains(t *testing.T, text string, needles ...string) { func assertContains(t *testing.T, text string, needles ...string) {
t.Helper() t.Helper()
for _, needle := range needles { for _, needle := range needles {
+24
View File
@@ -205,12 +205,17 @@ type HardwarePCIeDevice struct {
SFPVendor *string `json:"sfp_vendor,omitempty"` SFPVendor *string `json:"sfp_vendor,omitempty"`
SFPPartNumber *string `json:"sfp_part_number,omitempty"` SFPPartNumber *string `json:"sfp_part_number,omitempty"`
SFPSerialNumber *string `json:"sfp_serial_number,omitempty"` SFPSerialNumber *string `json:"sfp_serial_number,omitempty"`
SFPRevision *string `json:"sfp_revision,omitempty"`
SFPTransceiverType *string `json:"sfp_transceiver_type,omitempty"`
SFPWavelengthNM *float64 `json:"sfp_wavelength_nm,omitempty"` SFPWavelengthNM *float64 `json:"sfp_wavelength_nm,omitempty"`
SFPTemperatureC *float64 `json:"sfp_temperature_c,omitempty"` SFPTemperatureC *float64 `json:"sfp_temperature_c,omitempty"`
SFPTXPowerDBM *float64 `json:"sfp_tx_power_dbm,omitempty"` SFPTXPowerDBM *float64 `json:"sfp_tx_power_dbm,omitempty"`
SFPRXPowerDBM *float64 `json:"sfp_rx_power_dbm,omitempty"` SFPRXPowerDBM *float64 `json:"sfp_rx_power_dbm,omitempty"`
SFPVoltageV *float64 `json:"sfp_voltage_v,omitempty"` SFPVoltageV *float64 `json:"sfp_voltage_v,omitempty"`
SFPBiasMA *float64 `json:"sfp_bias_ma,omitempty"` SFPBiasMA *float64 `json:"sfp_bias_ma,omitempty"`
// SFPModules is the per-port transceiver inventory. The scalar SFP*
// fields above are retained for compatibility and mirror port 0 only.
SFPModules []HardwareSFPModule `json:"sfp_modules,omitempty"`
BDF *string `json:"-"` BDF *string `json:"-"`
DeviceClass *string `json:"device_class,omitempty"` DeviceClass *string `json:"device_class,omitempty"`
Manufacturer *string `json:"manufacturer,omitempty"` Manufacturer *string `json:"manufacturer,omitempty"`
@@ -227,6 +232,25 @@ type HardwarePCIeDevice struct {
Telemetry map[string]any `json:"-"` Telemetry map[string]any `json:"-"`
} }
// HardwareSFPModule is one optical/electrical transceiver attached to a NIC
// port. Port is the zero-based port number within its PCIe device.
type HardwareSFPModule struct {
Port int `json:"port"`
Identifier *string `json:"identifier,omitempty"`
Connector *string `json:"connector,omitempty"`
Vendor *string `json:"vendor,omitempty"`
PartNumber *string `json:"part_number,omitempty"`
SerialNumber *string `json:"serial_number,omitempty"`
Revision *string `json:"revision,omitempty"`
TransceiverType *string `json:"transceiver_type,omitempty"`
WavelengthNM *int `json:"wavelength_nm,omitempty"`
TemperatureC *float64 `json:"temperature_c,omitempty"`
TXPowerDBM *float64 `json:"tx_power_dbm,omitempty"`
RXPowerDBM *float64 `json:"rx_power_dbm,omitempty"`
VoltageV *float64 `json:"voltage_v,omitempty"`
BiasMA *float64 `json:"bias_ma,omitempty"`
}
type HardwarePowerSupply struct { type HardwarePowerSupply struct {
HardwareComponentStatus HardwareComponentStatus
Slot *string `json:"slot,omitempty"` Slot *string `json:"slot,omitempty"`
+2 -1
View File
@@ -9,6 +9,7 @@ if [ "${BEE_CONTAINER_BUILD:-0}" != "1" ]; then
fi fi
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
PROJECT_BUILD_COMMIT="$(git -C "${REPO_ROOT}" rev-parse --short=12 HEAD 2>/dev/null || echo unknown)"
BUILDER_DIR="${REPO_ROOT}/iso/builder" BUILDER_DIR="${REPO_ROOT}/iso/builder"
OVERLAY_DIR="${REPO_ROOT}/iso/overlay" OVERLAY_DIR="${REPO_ROOT}/iso/overlay"
DIST_DIR="${REPO_ROOT}/dist" DIST_DIR="${REPO_ROOT}/dist"
@@ -1364,7 +1365,7 @@ if [ "$NEED_BUILD" = "1" ]; then
"cd '${REPO_ROOT}/audit' && \ "cd '${REPO_ROOT}/audit' && \
env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \ env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
go build \ go build \
-ldflags '-s -w -X main.Version=${PROJECT_VERSION_EFFECTIVE}' \ -ldflags '-s -w -X main.Version=${PROJECT_VERSION_EFFECTIVE} -X main.BuildCommit=${PROJECT_BUILD_COMMIT}' \
-o '${BEE_BIN}' \ -o '${BEE_BIN}' \
./cmd/bee" ./cmd/bee"
echo "binary: $BEE_BIN" echo "binary: $BEE_BIN"
+2 -1
View File
@@ -42,8 +42,9 @@ echo "==> Сборка бинарника..."
( (
cd audit cd audit
VERSION=$(sh ./scripts/resolve-version.sh 2>/dev/null || echo "dev") VERSION=$(sh ./scripts/resolve-version.sh 2>/dev/null || echo "dev")
BUILD_COMMIT=$(git -C .. rev-parse --short=12 HEAD 2>/dev/null || echo "unknown")
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags "-X main.Version=${VERSION}" -o bee ./cmd/bee go build -ldflags "-X main.Version=${VERSION} -X main.BuildCommit=${BUILD_COMMIT}" -o bee ./cmd/bee
) )
echo " OK: $(ls -lh "${LOCAL_BIN}" | awk '{print $5, $9}')" echo " OK: $(ls -lh "${LOCAL_BIN}" | awk '{print $5, $9}')"