fix: complete hardware collection diagnostics
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
||||
)
|
||||
|
||||
var Version = "dev"
|
||||
var BuildCommit = "unknown"
|
||||
|
||||
func buildLabel() string {
|
||||
label := strings.TrimSpace(Version)
|
||||
@@ -92,6 +93,10 @@ func run(args []string, stdout, stderr io.Writer) (exitCode int) {
|
||||
case "gpu-bandwidth-groups":
|
||||
return runGPUBandwidthGroups(args[1:], stdout, stderr)
|
||||
case "version", "--version", "-version":
|
||||
if len(args) > 1 && args[1] == "--commit" {
|
||||
fmt.Fprintln(stdout, BuildCommit)
|
||||
return 0
|
||||
}
|
||||
fmt.Fprintln(stdout, Version)
|
||||
return 0
|
||||
default:
|
||||
|
||||
@@ -232,6 +232,11 @@ func ApplySATResultToDB(db *ComponentStatusDB, target, archivePath string) {
|
||||
if overall == "" {
|
||||
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
|
||||
dbStatus := satStatusToDBStatus(overall)
|
||||
|
||||
@@ -642,6 +642,7 @@ func writeManifest(dst, exportDir, stageRoot string) error {
|
||||
}
|
||||
var body strings.Builder
|
||||
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, "generated_at_utc=%s\n", time.Now().UTC().Format(time.RFC3339))
|
||||
fmt.Fprintf(&body, "export_dir=%s\n", exportDir)
|
||||
@@ -747,6 +748,16 @@ func buildVersion() string {
|
||||
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 {
|
||||
entries, err := os.ReadDir(srcDir)
|
||||
if err != nil {
|
||||
|
||||
@@ -34,6 +34,20 @@ var dmesgErrorPatterns = []*regexp.Regexp{
|
||||
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
|
||||
// returns only lines that match known error/warning patterns.
|
||||
func collectDmesgErrors() []schema.HardwareEventLog {
|
||||
@@ -77,6 +91,9 @@ func parseDmesgErrors(output string) []schema.HardwareEventLog {
|
||||
if !matchesAny(message, dmesgErrorPatterns) {
|
||||
continue
|
||||
}
|
||||
if matchesAny(message, dmesgIgnorePatterns) {
|
||||
continue
|
||||
}
|
||||
|
||||
severity := dmesgSeverity(message)
|
||||
source := "dmesg"
|
||||
@@ -114,10 +131,8 @@ func matchesAny(s string, patterns []*regexp.Regexp) bool {
|
||||
// "fault" (de-fault), and "undead" would contain "dead".
|
||||
var dmesgSeverityCriticalPatterns = []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?i)\bpanic\b`),
|
||||
regexp.MustCompile(`(?i)\baer\b`),
|
||||
regexp.MustCompile(`(?i)\buncorrect`),
|
||||
regexp.MustCompile(`(?i)\bxid\b`),
|
||||
regexp.MustCompile(`(?i)\bnvrm\b`),
|
||||
regexp.MustCompile(`(?i)\berror\b`),
|
||||
regexp.MustCompile(`(?i)\bfault\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.*",
|
||||
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 {
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -42,6 +43,7 @@ func enrichPCIeWithNICTelemetry(devs []schema.HardwarePCIeDevice) []schema.Hardw
|
||||
if len(ifaces) == 0 {
|
||||
continue
|
||||
}
|
||||
sort.Strings(ifaces)
|
||||
iface := ifaces[0]
|
||||
devs[i].MacAddresses = collectInterfaceMACs(ifaces)
|
||||
if devs[i].SerialNumber == nil {
|
||||
@@ -58,11 +60,24 @@ func enrichPCIeWithNICTelemetry(devs []schema.HardwarePCIeDevice) []schema.Hardw
|
||||
}
|
||||
}
|
||||
|
||||
if out, err := ethtoolModuleQuery(iface); err == nil {
|
||||
if injectSFPDOMTelemetry(&devs[i], out) {
|
||||
enriched++
|
||||
for port, portIface := range ifaces {
|
||||
out, err := ethtoolModuleQuery(portIface)
|
||||
if err != nil {
|
||||
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 {
|
||||
enriched++
|
||||
@@ -72,6 +87,27 @@ func enrichPCIeWithNICTelemetry(devs []schema.HardwarePCIeDevice) []schema.Hardw
|
||||
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 {
|
||||
if dev.DeviceClass == nil {
|
||||
return false
|
||||
@@ -140,6 +176,14 @@ func injectSFPDOMTelemetry(dev *schema.HardwarePCIeDevice, raw string) bool {
|
||||
s := strings.TrimSpace(val)
|
||||
dev.SFPSerialNumber = &s
|
||||
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"):
|
||||
if f, ok := firstFloat(val); ok {
|
||||
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) {
|
||||
raw := `
|
||||
05:00.0 Ethernet controller: Mellanox Technologies MT28908 Family [ConnectX-6]
|
||||
|
||||
@@ -39,7 +39,7 @@ func collectPSUs(manufacturer string) []schema.HardwarePowerSupply {
|
||||
if len(psus) == 0 {
|
||||
psus = synthesizePSUsFromSDR(sdrData)
|
||||
} else {
|
||||
mergePSUSDR(psus, sdrData)
|
||||
psus = mergePSUSDR(psus, sdrData)
|
||||
}
|
||||
} else if len(psus) == 0 {
|
||||
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:
|
||||
//
|
||||
// MSI: PSU1_POWER_IN, PSU1_PIN
|
||||
// MLT: PSU1_PIN
|
||||
// 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:
|
||||
//
|
||||
// MSI: PSU1_POWER_OUT
|
||||
// MLT: PSU1_POUT
|
||||
// xFusion: PS1 POut
|
||||
@@ -410,12 +412,14 @@ func synthesizePSUsFromSDR(sdr map[int]psuSDR) []schema.HardwarePowerSupply {
|
||||
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 {
|
||||
slotIdx, err := strconv.Atoi(derefPSUSlot(psus[i].Slot))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
matched[slotIdx+1] = true
|
||||
entry, ok := sdr[slotIdx+1]
|
||||
if !ok {
|
||||
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 {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package collector
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"bee/audit/internal/schema"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParsePSUSDR(t *testing.T) {
|
||||
raw := `
|
||||
@@ -118,3 +121,15 @@ func TestSynthesizePSUsFromSDR(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,21 +652,56 @@ func appendUniqueStorage(base, extra []schema.HardwareStorage) []schema.Hardware
|
||||
if len(extra) == 0 {
|
||||
return base
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, d := range base {
|
||||
seen[storageIdentityKey(d)] = true
|
||||
seen := map[string]int{}
|
||||
for i, d := range base {
|
||||
if key := storageIdentityKey(d); key != "" {
|
||||
seen[key] = i
|
||||
}
|
||||
}
|
||||
for _, d := range extra {
|
||||
key := storageIdentityKey(d)
|
||||
if key == "" || seen[key] {
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if idx, ok := seen[key]; ok {
|
||||
mergeStorageRecord(&base[idx], d)
|
||||
continue
|
||||
}
|
||||
base = append(base, d)
|
||||
seen[key] = true
|
||||
seen[key] = len(base) - 1
|
||||
}
|
||||
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 {
|
||||
if d.SerialNumber != nil && strings.TrimSpace(*d.SerialNumber) != "" {
|
||||
return "sn:" + strings.ToLower(strings.TrimSpace(*d.SerialNumber))
|
||||
|
||||
@@ -6,6 +6,16 @@ import (
|
||||
"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) {
|
||||
raw := `LSI Corporation SAS2 IR Configuration Utility.
|
||||
Adapter List
|
||||
|
||||
@@ -43,6 +43,9 @@ func (s *System) RunNvidiaConfigCheckPack(ctx context.Context, baseDir string, l
|
||||
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())
|
||||
} else {
|
||||
if len(settings) == 0 {
|
||||
status.NoGPUs = true
|
||||
}
|
||||
for _, g := range settings {
|
||||
f := NvidiaGPUConfigFinding{
|
||||
Index: g.Index, Name: g.Name,
|
||||
@@ -127,6 +130,7 @@ type NvidiaConfigCheckStatus struct {
|
||||
|
||||
GPUs []NvidiaGPUConfigFinding `json:"gpus,omitempty"`
|
||||
NVLinkPairs []NvidiaNVLinkPairFinding `json:"nvlink_pairs,omitempty"`
|
||||
NoGPUs bool `json:"no_gpus"`
|
||||
|
||||
// Confidential Computing readiness — informational only, does not gate
|
||||
// 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, "cpu_cc_capability=%s\n", status.CPUCCCapability)
|
||||
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")
|
||||
} else {
|
||||
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") {
|
||||
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
|
||||
|
||||
@@ -920,6 +920,12 @@ func (s *System) RunStorageAcceptancePack(ctx context.Context, baseDir string, e
|
||||
runSyncBracketHook(job, "after", logFunc)
|
||||
}
|
||||
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)
|
||||
key := filepath.Base(devPath) + "_" + strings.ReplaceAll(job.name, "-", "_")
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
start := time.Now().UTC()
|
||||
resolvedCmd, err := resolveSATCommand(cmd)
|
||||
|
||||
@@ -124,6 +124,7 @@ func writeNVMeReport(b *strings.Builder, outputs map[string][]byte) {
|
||||
writtenBytes: writtenBytes,
|
||||
readBytes: readBytes,
|
||||
capacityBytes: capacityBytes,
|
||||
healthKnown: true,
|
||||
}
|
||||
writeResourceSection(b, ri)
|
||||
|
||||
@@ -232,6 +233,7 @@ func writeSATAReport(b *strings.Builder, outputs map[string][]byte) {
|
||||
capacityBytes: capacityBytes,
|
||||
readPercent: 100 - readValue,
|
||||
hasReadPercent: hasReadValue,
|
||||
healthKnown: !strings.EqualFold(health, "unknown"),
|
||||
}
|
||||
writeResourceSection(b, ri)
|
||||
|
||||
@@ -341,6 +343,7 @@ const (
|
||||
|
||||
type resourceInfo struct {
|
||||
powerOnHours uint64
|
||||
healthKnown bool
|
||||
powerCycles uint64
|
||||
writtenBytes uint64
|
||||
readBytes uint64
|
||||
@@ -393,7 +396,10 @@ func writeConclusionSection(b *strings.Builder, r resourceInfo) {
|
||||
writeSectionHeader(b, "Conclusion")
|
||||
|
||||
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 {
|
||||
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)))
|
||||
}
|
||||
|
||||
if isNew {
|
||||
if !r.healthKnown {
|
||||
writeField(b, "Disk Condition", "UNVERIFIED")
|
||||
} else if isNew {
|
||||
writeField(b, "Disk Condition", "NEW")
|
||||
} else {
|
||||
writeField(b, "Disk Condition", "USED")
|
||||
|
||||
@@ -141,6 +141,15 @@ func TestGenerateDiskReportSATA(t *testing.T) {
|
||||
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) {
|
||||
t.Helper()
|
||||
for _, needle := range needles {
|
||||
|
||||
@@ -205,12 +205,17 @@ type HardwarePCIeDevice struct {
|
||||
SFPVendor *string `json:"sfp_vendor,omitempty"`
|
||||
SFPPartNumber *string `json:"sfp_part_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"`
|
||||
SFPTemperatureC *float64 `json:"sfp_temperature_c,omitempty"`
|
||||
SFPTXPowerDBM *float64 `json:"sfp_tx_power_dbm,omitempty"`
|
||||
SFPRXPowerDBM *float64 `json:"sfp_rx_power_dbm,omitempty"`
|
||||
SFPVoltageV *float64 `json:"sfp_voltage_v,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:"-"`
|
||||
DeviceClass *string `json:"device_class,omitempty"`
|
||||
Manufacturer *string `json:"manufacturer,omitempty"`
|
||||
@@ -227,6 +232,25 @@ type HardwarePCIeDevice struct {
|
||||
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 {
|
||||
HardwareComponentStatus
|
||||
Slot *string `json:"slot,omitempty"`
|
||||
|
||||
@@ -9,6 +9,7 @@ if [ "${BEE_CONTAINER_BUILD:-0}" != "1" ]; then
|
||||
fi
|
||||
|
||||
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"
|
||||
OVERLAY_DIR="${REPO_ROOT}/iso/overlay"
|
||||
DIST_DIR="${REPO_ROOT}/dist"
|
||||
@@ -1364,7 +1365,7 @@ if [ "$NEED_BUILD" = "1" ]; then
|
||||
"cd '${REPO_ROOT}/audit' && \
|
||||
env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
|
||||
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}' \
|
||||
./cmd/bee"
|
||||
echo "binary: $BEE_BIN"
|
||||
|
||||
+2
-1
@@ -42,8 +42,9 @@ echo "==> Сборка бинарника..."
|
||||
(
|
||||
cd audit
|
||||
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 \
|
||||
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}')"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user