Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b98005d5d | |||
| 33bc275da2 |
@@ -766,7 +766,7 @@ func parseMDAdmPlatformLicense(raw string) *string {
|
||||
|
||||
func queryDeviceSerial(devPath string) string {
|
||||
if out, err := exec.Command("nvme", "id-ctrl", devPath, "-o", "json").Output(); err == nil {
|
||||
var ctrl nvmeIDCtrl
|
||||
var ctrl NVMeIDCtrl
|
||||
if json.Unmarshal(out, &ctrl) == nil {
|
||||
if v := cleanDMIValue(strings.TrimSpace(ctrl.SerialNumber)); v != "" {
|
||||
return v
|
||||
|
||||
@@ -84,16 +84,19 @@ func collectStorage() []schema.HardwareStorage {
|
||||
return result
|
||||
}
|
||||
|
||||
// jsonInt64 accepts both a bare JSON number and a JSON-quoted number string.
|
||||
// lsblk -J emits LOG-SEC / PHY-SEC as integers on util-linux ≥ 2.37 (Debian 12)
|
||||
// but older versions emit them as strings. This type handles both.
|
||||
type jsonInt64 int64
|
||||
// JSONInt64 accepts a bare JSON number (512), a JSON-quoted number string
|
||||
// ("512" — lsblk -J on util-linux < 2.37, and nvme-cli for large 64-bit
|
||||
// counters that would lose precision as JS numbers), or a {"lo":n,"hi":n}
|
||||
// object (128-bit NVMe counters on some nvme-cli versions; hi is ignored as
|
||||
// no real counter exceeds 64 bits). Shared by lsblk and nvme-cli JSON output
|
||||
// across the collector and the human-readable disk report.
|
||||
type JSONInt64 int64
|
||||
|
||||
func (j *jsonInt64) UnmarshalJSON(data []byte) error {
|
||||
func (j *JSONInt64) UnmarshalJSON(data []byte) error {
|
||||
// bare number: 512
|
||||
var n int64
|
||||
if err := json.Unmarshal(data, &n); err == nil {
|
||||
*j = jsonInt64(n)
|
||||
*j = JSONInt64(n)
|
||||
return nil
|
||||
}
|
||||
// quoted string: "512"
|
||||
@@ -101,24 +104,32 @@ func (j *jsonInt64) UnmarshalJSON(data []byte) error {
|
||||
if err := json.Unmarshal(data, &s); err == nil {
|
||||
n, err := strconv.ParseInt(strings.TrimSpace(s), 10, 64)
|
||||
if err == nil {
|
||||
*j = jsonInt64(n)
|
||||
*j = JSONInt64(n)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// {"lo":n,"hi":n} 128-bit counter object
|
||||
var obj struct {
|
||||
Lo int64 `json:"lo"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &obj); err == nil {
|
||||
*j = JSONInt64(obj.Lo)
|
||||
return nil
|
||||
}
|
||||
return nil // null or unexpected type — leave zero
|
||||
}
|
||||
|
||||
// lsblkDevice is a minimal lsblk JSON record.
|
||||
type lsblkDevice struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Size string `json:"size"`
|
||||
Serial string `json:"serial"`
|
||||
Model string `json:"model"`
|
||||
Tran string `json:"tran"`
|
||||
Hctl string `json:"hctl"`
|
||||
LogSec jsonInt64 `json:"log-sec"`
|
||||
PhySec jsonInt64 `json:"phy-sec"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Size string `json:"size"`
|
||||
Serial string `json:"serial"`
|
||||
Model string `json:"model"`
|
||||
Tran string `json:"tran"`
|
||||
Hctl string `json:"hctl"`
|
||||
LogSec JSONInt64 `json:"log-sec"`
|
||||
PhySec JSONInt64 `json:"phy-sec"`
|
||||
}
|
||||
|
||||
type lsblkRoot struct {
|
||||
@@ -423,32 +434,36 @@ func enrichWithSmartctl(dev lsblkDevice) schema.HardwareStorage {
|
||||
return s
|
||||
}
|
||||
|
||||
// nvmeSmartLog is the subset of `nvme smart-log -o json` output we care about.
|
||||
// nvme-cli emits most counters as JSON strings (e.g. "power_on_hours":"49"),
|
||||
// so all numeric fields use jsonInt64 which accepts both bare numbers and
|
||||
// quoted strings. Field names match nvme-cli JSON output, not NVMe spec prose.
|
||||
type nvmeSmartLog struct {
|
||||
CriticalWarning jsonInt64 `json:"critical_warning"`
|
||||
PercentageUsed jsonInt64 `json:"percent_used"`
|
||||
AvailableSpare jsonInt64 `json:"avail_spare"`
|
||||
SpareThreshold jsonInt64 `json:"spare_thresh"`
|
||||
Temperature jsonInt64 `json:"temperature"`
|
||||
PowerOnHours jsonInt64 `json:"power_on_hours"`
|
||||
PowerCycles jsonInt64 `json:"power_cycles"`
|
||||
UnsafeShutdowns jsonInt64 `json:"unsafe_shutdowns"`
|
||||
DataUnitsRead jsonInt64 `json:"data_units_read"`
|
||||
DataUnitsWritten jsonInt64 `json:"data_units_written"`
|
||||
ControllerBusy jsonInt64 `json:"controller_busy_time"`
|
||||
MediaErrors jsonInt64 `json:"media_errors"`
|
||||
NumErrLogEntries jsonInt64 `json:"num_err_log_entries"`
|
||||
// NVMeSmartLog is the subset of `nvme smart-log -o json` output shared by the
|
||||
// structured collector and the human-readable disk report. nvme-cli emits
|
||||
// most counters as JSON strings (e.g. "power_on_hours":"49") or, on some
|
||||
// versions, as {"lo":n,"hi":n} objects — all numeric fields use JSONInt64,
|
||||
// which accepts bare numbers, quoted strings, and lo/hi objects. Field names
|
||||
// match nvme-cli JSON output, not NVMe spec prose.
|
||||
type NVMeSmartLog struct {
|
||||
CriticalWarning JSONInt64 `json:"critical_warning"`
|
||||
PercentageUsed JSONInt64 `json:"percent_used"`
|
||||
AvailableSpare JSONInt64 `json:"avail_spare"`
|
||||
SpareThreshold JSONInt64 `json:"spare_thresh"`
|
||||
Temperature JSONInt64 `json:"temperature"`
|
||||
PowerOnHours JSONInt64 `json:"power_on_hours"`
|
||||
PowerCycles JSONInt64 `json:"power_cycles"`
|
||||
UnsafeShutdowns JSONInt64 `json:"unsafe_shutdowns"`
|
||||
DataUnitsRead JSONInt64 `json:"data_units_read"`
|
||||
DataUnitsWritten JSONInt64 `json:"data_units_written"`
|
||||
ControllerBusy JSONInt64 `json:"controller_busy_time"`
|
||||
MediaErrors JSONInt64 `json:"media_errors"`
|
||||
NumErrLogEntries JSONInt64 `json:"num_err_log_entries"`
|
||||
}
|
||||
|
||||
// nvmeIDCtrl is the subset of `nvme id-ctrl -o json` output.
|
||||
type nvmeIDCtrl struct {
|
||||
ModelNumber string `json:"mn"`
|
||||
SerialNumber string `json:"sn"`
|
||||
FirmwareRev string `json:"fr"`
|
||||
TotalCapacity int64 `json:"tnvmcap"`
|
||||
// NVMeIDCtrl is the subset of `nvme id-ctrl -o json` output shared by the
|
||||
// structured collector and the human-readable disk report.
|
||||
type NVMeIDCtrl struct {
|
||||
ModelNumber string `json:"mn"`
|
||||
SerialNumber string `json:"sn"`
|
||||
FirmwareRev string `json:"fr"`
|
||||
TotalCapacity JSONInt64 `json:"tnvmcap"`
|
||||
NVMCapacity JSONInt64 `json:"nvmcap"`
|
||||
}
|
||||
|
||||
func enrichWithNVMe(dev lsblkDevice) schema.HardwareStorage {
|
||||
@@ -481,7 +496,7 @@ func enrichWithNVMe(dev lsblkDevice) schema.HardwareStorage {
|
||||
|
||||
// id-ctrl: model, serial, firmware, capacity
|
||||
if out, err := exec.Command("nvme", "id-ctrl", devPath, "-o", "json").Output(); err == nil {
|
||||
var ctrl nvmeIDCtrl
|
||||
var ctrl NVMeIDCtrl
|
||||
if json.Unmarshal(out, &ctrl) == nil {
|
||||
if v := cleanDMIValue(strings.TrimSpace(ctrl.ModelNumber)); v != "" {
|
||||
s.Model = &v
|
||||
@@ -502,7 +517,7 @@ func enrichWithNVMe(dev lsblkDevice) schema.HardwareStorage {
|
||||
|
||||
// smart-log: wear telemetry
|
||||
if out, err := exec.Command("nvme", "smart-log", devPath, "-o", "json").Output(); err == nil {
|
||||
var log nvmeSmartLog
|
||||
var log NVMeSmartLog
|
||||
if json.Unmarshal(out, &log) == nil {
|
||||
if log.PowerOnHours > 0 {
|
||||
v := int64(log.PowerOnHours)
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestJsonInt64UnmarshalBothFormats(t *testing.T) {
|
||||
{`null`, 0},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
var v jsonInt64
|
||||
var v JSONInt64
|
||||
if err := v.UnmarshalJSON([]byte(tc.json)); err != nil {
|
||||
t.Fatalf("UnmarshalJSON(%s): unexpected error %v", tc.json, err)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
// TestNVMeSmartLogUnmarshal verifies that nvme-cli JSON output (where most
|
||||
// counters are quoted strings and field names differ from NVMe spec prose)
|
||||
// is correctly parsed into nvmeSmartLog.
|
||||
// is correctly parsed into NVMeSmartLog.
|
||||
func TestNVMeSmartLogUnmarshal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestNVMeSmartLogUnmarshal(t *testing.T) {
|
||||
"media_errors": "0",
|
||||
"num_err_log_entries": "0"
|
||||
}`
|
||||
var log nvmeSmartLog
|
||||
var log NVMeSmartLog
|
||||
if err := json.Unmarshal([]byte(raw), &log); err != nil {
|
||||
t.Fatalf("json.Unmarshal failed: %v", err)
|
||||
}
|
||||
|
||||
@@ -1259,7 +1259,7 @@ func storageSATCommands(devPath string, extended bool) []satJob {
|
||||
return jobs
|
||||
}
|
||||
jobs := []satJob{
|
||||
{name: "smartctl-health", cmd: []string{"smartctl", "-H", "-A", devPath}},
|
||||
{name: "smartctl-health", cmd: []string{"smartctl", "-H", "-A", "-i", devPath}},
|
||||
}
|
||||
if extended {
|
||||
jobs = append(jobs, satJob{name: "smartctl-self-test-short", cmd: []string{"smartctl", "-t", "short", devPath}})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"bee/audit/internal/collector"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
@@ -39,65 +40,22 @@ func GenerateDiskReportText(index int, devPath string, outputs map[string][]byte
|
||||
|
||||
// ── NVMe ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
type nvmeIdCtrl struct {
|
||||
ModelNumber string `json:"mn"`
|
||||
SerialNumber string `json:"sn"`
|
||||
Firmware string `json:"fr"`
|
||||
TotalCap uint64 `json:"tnvmcap"`
|
||||
NVMCap uint64 `json:"nvmcap"`
|
||||
}
|
||||
|
||||
// nvmeU64 handles both plain JSON numbers and {"lo":n,"hi":n} objects that
|
||||
// some nvme-cli versions emit for 128-bit counters.
|
||||
func nvmeU64(raw json.RawMessage) uint64 {
|
||||
if len(raw) == 0 {
|
||||
return 0
|
||||
}
|
||||
var n uint64
|
||||
if json.Unmarshal(raw, &n) == nil {
|
||||
return n
|
||||
}
|
||||
var obj struct {
|
||||
Lo uint64 `json:"lo"`
|
||||
Hi uint64 `json:"hi"`
|
||||
}
|
||||
if json.Unmarshal(raw, &obj) == nil {
|
||||
return obj.Lo
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type nvmeSmartLogRaw struct {
|
||||
CriticalWarning uint64 `json:"critical_warning"`
|
||||
Temperature json.RawMessage `json:"temperature"`
|
||||
AvailSpare uint64 `json:"avail_spare"`
|
||||
SpareThresh uint64 `json:"spare_thresh"`
|
||||
PercentUsed uint64 `json:"percent_used"`
|
||||
DataUnitsRead json.RawMessage `json:"data_units_read"`
|
||||
DataUnitsWritten json.RawMessage `json:"data_units_written"`
|
||||
PowerCycles json.RawMessage `json:"power_cycles"`
|
||||
PowerOnHours json.RawMessage `json:"power_on_hours"`
|
||||
UnsafeShutdowns json.RawMessage `json:"unsafe_shutdowns"`
|
||||
MediaErrors json.RawMessage `json:"media_errors"`
|
||||
NumErrLogEntries json.RawMessage `json:"num_err_log_entries"`
|
||||
}
|
||||
|
||||
func writeNVMeReport(b *strings.Builder, outputs map[string][]byte) {
|
||||
// id-ctrl
|
||||
var ctrl nvmeIdCtrl
|
||||
var ctrl collector.NVMeIDCtrl
|
||||
if data := outputs["nvme-id-ctrl"]; len(data) > 0 {
|
||||
_ = json.Unmarshal(data, &ctrl)
|
||||
}
|
||||
|
||||
model := strings.TrimSpace(ctrl.ModelNumber)
|
||||
serial := strings.TrimSpace(ctrl.SerialNumber)
|
||||
firmware := strings.TrimSpace(ctrl.Firmware)
|
||||
firmware := strings.TrimSpace(ctrl.FirmwareRev)
|
||||
|
||||
capacityGB := ""
|
||||
if ctrl.TotalCap > 0 {
|
||||
capacityGB = formatCapacityGB(ctrl.TotalCap)
|
||||
} else if ctrl.NVMCap > 0 {
|
||||
capacityGB = formatCapacityGB(ctrl.NVMCap)
|
||||
if ctrl.TotalCapacity > 0 {
|
||||
capacityGB = formatCapacityGB(uint64(ctrl.TotalCapacity))
|
||||
} else if ctrl.NVMCapacity > 0 {
|
||||
capacityGB = formatCapacityGB(uint64(ctrl.NVMCapacity))
|
||||
}
|
||||
|
||||
writeField(b, "Model", model)
|
||||
@@ -113,67 +71,69 @@ func writeNVMeReport(b *strings.Builder, outputs map[string][]byte) {
|
||||
b.WriteString("\n(no SMART data)\n")
|
||||
return
|
||||
}
|
||||
var sl nvmeSmartLogRaw
|
||||
var sl collector.NVMeSmartLog
|
||||
if err := json.Unmarshal(data, &sl); err != nil {
|
||||
fmt.Fprintf(b, "\n(SMART parse error: %v)\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
tempK := nvmeU64(sl.Temperature)
|
||||
tempC := int(tempK) - 273
|
||||
tempC := int(sl.Temperature) - 273
|
||||
if tempC < 0 {
|
||||
tempC = 0
|
||||
}
|
||||
|
||||
critWarn := sl.CriticalWarning
|
||||
critWarnStr := "OK"
|
||||
if critWarn != 0 {
|
||||
critWarnStr = fmt.Sprintf("0x%02X", critWarn)
|
||||
if sl.CriticalWarning != 0 {
|
||||
critWarnStr = fmt.Sprintf("0x%02X", sl.CriticalWarning)
|
||||
}
|
||||
|
||||
poh := nvmeU64(sl.PowerOnHours)
|
||||
pc := nvmeU64(sl.PowerCycles)
|
||||
us := nvmeU64(sl.UnsafeShutdowns)
|
||||
me := nvmeU64(sl.MediaErrors)
|
||||
nel := nvmeU64(sl.NumErrLogEntries)
|
||||
poh := uint64(sl.PowerOnHours)
|
||||
pc := uint64(sl.PowerCycles)
|
||||
us := uint64(sl.UnsafeShutdowns)
|
||||
me := uint64(sl.MediaErrors)
|
||||
nel := uint64(sl.NumErrLogEntries)
|
||||
|
||||
// data_units are in 1000 × 512-byte sectors = 512,000 bytes each
|
||||
dataRead := float64(nvmeU64(sl.DataUnitsRead)) * 512000 / 1e9
|
||||
dataWritten := float64(nvmeU64(sl.DataUnitsWritten)) * 512000 / 1e9
|
||||
readBytes := uint64(sl.DataUnitsRead) * 512000
|
||||
writtenBytes := uint64(sl.DataUnitsWritten) * 512000
|
||||
|
||||
writeSectionHeader(b, "Health")
|
||||
writeField(b, "Temperature", fmt.Sprintf("%d °C", tempC))
|
||||
writeField(b, "Critical Warning", critWarnStr)
|
||||
writeField(b, "Percentage Used", fmt.Sprintf("%d %%", sl.PercentUsed))
|
||||
writeField(b, "Available Spare", fmt.Sprintf("%d %% (threshold: %d %%)", sl.AvailSpare, sl.SpareThresh))
|
||||
writeField(b, "Percentage Used", fmt.Sprintf("%d %%", sl.PercentageUsed))
|
||||
writeField(b, "Available Spare", fmt.Sprintf("%d %% (threshold: %d %%)", sl.AvailableSpare, sl.SpareThreshold))
|
||||
|
||||
writeSectionHeader(b, "Usage")
|
||||
writeField(b, "Power On Hours", fmt.Sprintf("%s h", formatUint(poh)))
|
||||
writeField(b, "Power Cycles", formatUint(pc))
|
||||
writeField(b, "Unsafe Shutdowns", formatUint(us))
|
||||
writeField(b, "Data Written", fmt.Sprintf("%.1f GB", dataWritten))
|
||||
writeField(b, "Data Read", fmt.Sprintf("%.1f GB", dataRead))
|
||||
writeField(b, "Data Written", formatBytesHuman(float64(writtenBytes)))
|
||||
writeField(b, "Data Read", formatBytesHuman(float64(readBytes)))
|
||||
|
||||
writeSectionHeader(b, "Errors")
|
||||
writeField(b, "Media Errors", formatUint(me))
|
||||
writeField(b, "Error Log Entries", formatUint(nel))
|
||||
|
||||
capacityBytes := ctrl.TotalCap
|
||||
capacityBytes := uint64(ctrl.TotalCapacity)
|
||||
if capacityBytes == 0 {
|
||||
capacityBytes = ctrl.NVMCap
|
||||
capacityBytes = uint64(ctrl.NVMCapacity)
|
||||
}
|
||||
writeResourceSection(b, resourceInfo{
|
||||
ri := resourceInfo{
|
||||
powerOnHours: poh,
|
||||
writtenBytes: uint64(nvmeU64(sl.DataUnitsWritten)) * 512000,
|
||||
readBytes: uint64(nvmeU64(sl.DataUnitsRead)) * 512000,
|
||||
powerCycles: pc,
|
||||
writtenBytes: writtenBytes,
|
||||
readBytes: readBytes,
|
||||
capacityBytes: capacityBytes,
|
||||
})
|
||||
}
|
||||
writeResourceSection(b, ri)
|
||||
|
||||
if selfTest := outputs["nvme-device-self-test"]; len(selfTest) > 0 {
|
||||
writeSectionHeader(b, "Self-Test")
|
||||
result := parseSelfTestResult(string(selfTest))
|
||||
writeField(b, "Result", result)
|
||||
}
|
||||
|
||||
writeConclusionSection(b, ri)
|
||||
}
|
||||
|
||||
// ── SATA / SAS (smartctl) ────────────────────────────────────────────────────
|
||||
@@ -246,13 +206,15 @@ func writeSATAReport(b *strings.Builder, outputs map[string][]byte) {
|
||||
}
|
||||
}
|
||||
|
||||
var poh, writtenLBAs, readLBAs uint64
|
||||
var poh, pc, writtenLBAs, readLBAs uint64
|
||||
var readValue int
|
||||
hasReadValue := false
|
||||
for _, a := range attrs {
|
||||
switch a.ID {
|
||||
case 9: // Power_On_Hours
|
||||
poh = parseLeadingUint(a.Raw)
|
||||
case 12: // Power_Cycle_Count
|
||||
pc = parseLeadingUint(a.Raw)
|
||||
case 241: // Total_LBAs_Written
|
||||
writtenLBAs = parseLeadingUint(a.Raw)
|
||||
case 242: // Total_LBAs_Read
|
||||
@@ -262,14 +224,16 @@ func writeSATAReport(b *strings.Builder, outputs map[string][]byte) {
|
||||
}
|
||||
}
|
||||
const sataSectorBytes = 512
|
||||
writeResourceSection(b, resourceInfo{
|
||||
ri := resourceInfo{
|
||||
powerOnHours: poh,
|
||||
powerCycles: pc,
|
||||
writtenBytes: writtenLBAs * sataSectorBytes,
|
||||
readBytes: readLBAs * sataSectorBytes,
|
||||
capacityBytes: capacityBytes,
|
||||
readPercent: 100 - readValue,
|
||||
hasReadPercent: hasReadValue,
|
||||
})
|
||||
}
|
||||
writeResourceSection(b, ri)
|
||||
|
||||
selfTest := outputs["smartctl-self-test-status"]
|
||||
if len(selfTest) == 0 {
|
||||
@@ -280,6 +244,8 @@ func writeSATAReport(b *strings.Builder, outputs map[string][]byte) {
|
||||
result := parseSelfTestResult(string(selfTest))
|
||||
writeField(b, "Result", result)
|
||||
}
|
||||
|
||||
writeConclusionSection(b, ri)
|
||||
}
|
||||
|
||||
func parseSMARTAttrs(text string) []smartAttr {
|
||||
@@ -375,6 +341,7 @@ const (
|
||||
|
||||
type resourceInfo struct {
|
||||
powerOnHours uint64
|
||||
powerCycles uint64
|
||||
writtenBytes uint64
|
||||
readBytes uint64
|
||||
capacityBytes uint64
|
||||
@@ -407,6 +374,70 @@ func writeResourceSection(b *strings.Builder, r resourceInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Conclusion (new-vs-used verdict) ────────────────────────────────────────
|
||||
|
||||
// Thresholds for treating a drive as "new": less than one full drive-write
|
||||
// (110% of capacity, headroom for provisioning/overprovisioning rounding),
|
||||
// less than a bit over two full drive-reads (210% of capacity), under a
|
||||
// week of power-on time, and under 30 power cycles. Any one violation is
|
||||
// enough to call the drive used — these are deliberately loose bounds, not
|
||||
// a wear/endurance judgment (see -- Resource -- for that).
|
||||
const (
|
||||
newDiskMaxWrittenFrac = 1.10
|
||||
newDiskMaxReadFrac = 2.10
|
||||
newDiskMaxUptimeHours = 7 * 24
|
||||
newDiskMaxPowerCycles = 30
|
||||
)
|
||||
|
||||
func writeConclusionSection(b *strings.Builder, r resourceInfo) {
|
||||
writeSectionHeader(b, "Conclusion")
|
||||
|
||||
var reasons, notes []string
|
||||
isNew := true
|
||||
|
||||
if r.capacityBytes > 0 {
|
||||
writtenFrac := float64(r.writtenBytes) / float64(r.capacityBytes)
|
||||
readFrac := float64(r.readBytes) / float64(r.capacityBytes)
|
||||
if writtenFrac >= newDiskMaxWrittenFrac {
|
||||
isNew = false
|
||||
reasons = append(reasons, fmt.Sprintf(
|
||||
"data written %s (%s of capacity)",
|
||||
formatBytesHuman(float64(r.writtenBytes)), formatPercent(writtenFrac*100)))
|
||||
}
|
||||
if readFrac >= newDiskMaxReadFrac {
|
||||
isNew = false
|
||||
reasons = append(reasons, fmt.Sprintf(
|
||||
"data read %s (%s of capacity)",
|
||||
formatBytesHuman(float64(r.readBytes)), formatPercent(readFrac*100)))
|
||||
}
|
||||
} else {
|
||||
notes = append(notes, "capacity unknown — write/read criteria not evaluated")
|
||||
}
|
||||
|
||||
if r.powerOnHours >= newDiskMaxUptimeHours {
|
||||
isNew = false
|
||||
reasons = append(reasons, fmt.Sprintf("uptime %s", formatHoursHuman(r.powerOnHours)))
|
||||
}
|
||||
|
||||
if r.powerCycles >= newDiskMaxPowerCycles {
|
||||
isNew = false
|
||||
reasons = append(reasons, fmt.Sprintf("power cycles %s", formatUint(r.powerCycles)))
|
||||
}
|
||||
|
||||
if isNew {
|
||||
writeField(b, "Disk Condition", "NEW")
|
||||
} else {
|
||||
writeField(b, "Disk Condition", "USED")
|
||||
b.WriteString(" Reason:\n")
|
||||
for _, reason := range reasons {
|
||||
fmt.Fprintf(b, " - %s\n", reason)
|
||||
}
|
||||
}
|
||||
for _, note := range notes {
|
||||
fmt.Fprintf(b, " Note: %s\n", note)
|
||||
}
|
||||
}
|
||||
|
||||
// progressBar renders a fixed-width pseudographic bar, e.g. "[######------]".
|
||||
func progressBar(frac float64, width int) string {
|
||||
if math.IsNaN(frac) || frac < 0 {
|
||||
|
||||
@@ -83,7 +83,36 @@ func TestGenerateDiskReportNVMe(t *testing.T) {
|
||||
assertContains(t, report, "1,234 h") // power_on_hours with separator
|
||||
assertContains(t, report, "32") // power_cycles
|
||||
assertContains(t, report, "3") // unsafe_shutdowns
|
||||
assertContains(t, report, "378.0 GB") // data_units_written * 512000 / 1e9
|
||||
assertContains(t, report, "378.00 GB") // data_units_written * 512000, human-scaled
|
||||
}
|
||||
|
||||
// TestGenerateDiskReportNVMeDataUnitsScaleToTB verifies that heavy write/read
|
||||
// counters render in the "-- Usage --" section as TB/PB, not raw GB, matching
|
||||
// the "-- Resource --" section which already used formatBytesHuman.
|
||||
func TestGenerateDiskReportNVMeDataUnitsScaleToTB(t *testing.T) {
|
||||
t.Parallel()
|
||||
heavy := []byte(`{
|
||||
"critical_warning": 0,
|
||||
"temperature": 307,
|
||||
"avail_spare": 100,
|
||||
"spare_thresh": 10,
|
||||
"percent_used": 0,
|
||||
"data_units_read": "252420478",
|
||||
"data_units_written": "103834055",
|
||||
"power_cycles": "45",
|
||||
"power_on_hours": "45",
|
||||
"unsafe_shutdowns": "35",
|
||||
"media_errors": "0",
|
||||
"num_err_log_entries": "0"
|
||||
}`)
|
||||
outputs := map[string][]byte{
|
||||
"nvme-id-ctrl": testNVMeIdCtrl,
|
||||
"nvme-smart-log": heavy,
|
||||
}
|
||||
report := GenerateDiskReportText(1, "/dev/nvme0n1", outputs, time.Unix(0, 0).UTC())
|
||||
|
||||
assertContains(t, report, "Data Written : 53.16 TB")
|
||||
assertContains(t, report, "Data Read : 129.24 TB")
|
||||
}
|
||||
|
||||
func TestGenerateDiskReportNVMeLoHi(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user