drive-health-metrics/discover.go
James Coleman ddafa90a02
Some checks failed
Go package / build (push) Has been cancelled
first commit
2026-06-22 17:16:34 -05:00

180 lines
5.5 KiB
Go

package main
import (
"regexp"
"strconv"
"strings"
)
// smartTool holds the resolved smartctl binary and whether it can emit JSON
// (smartmontools >= 7.0). On CentOS 6/7 jsonCapable is false and we parse text.
type smartTool struct {
bin string
jsonCapable bool
}
// newSmartTool resolves the smartctl binary and detects whether it can emit JSON
// (smartmontools >= 7.0); older builds fall back to text parsing.
func newSmartTool() smartTool {
bin := lookPath("smartctl", "/usr/sbin/smartctl", "/sbin/smartctl", "/usr/local/sbin/smartctl")
if bin == "" {
bin = "smartctl"
}
st := smartTool{bin: bin}
ver := run(bin, "--version")
// "smartctl 7.2 2020-12-30 r5155 ..."
if m := regexp.MustCompile(`smartctl\s+(\d+)\.(\d+)`).FindStringSubmatch(ver); m != nil {
major, _ := strconv.Atoi(m[1])
if major >= 7 {
st.jsonCapable = true
}
}
return st
}
// scanned describes one device from `smartctl --scan-open`.
type scanned struct {
path string
dtype string
megaraidN string // megaraidN is empty when the device is not a megaraid passthrough.
comment string
}
var scanLine = regexp.MustCompile(`^(\S+)\s+-d\s+(\S+)\s*#?(.*)$`)
var megaraidIdx = regexp.MustCompile(`megaraid,(\d+)`)
// scan enumerates physical drives from `smartctl --scan-open`, keeping direct
// SATA/SAS/NVMe devices and megaraid passthroughs while skipping iSCSI virtual
// disks.
func (s smartTool) scan() []scanned {
out := run(s.bin, "--scan-open")
var res []scanned
for _, ln := range strings.Split(out, "\n") {
ln = strings.TrimSpace(ln)
if ln == "" || strings.HasPrefix(ln, "#") {
continue
}
m := scanLine.FindStringSubmatch(ln)
if m == nil {
continue
}
path, dtype, comment := m[1], m[2], m[3]
if strings.Contains(strings.ToUpper(comment), "VIRTUAL-DISK") {
continue // Skip iSCSI IET virtual disks; they are not physical drives.
}
sc := scanned{path: path, dtype: dtype, comment: comment}
if mn := megaraidIdx.FindStringSubmatch(dtype); mn != nil {
sc.megaraidN = mn[1]
}
res = append(res, sc)
}
return res
}
// querySmart runs smartctl against a device. With a JSON-capable smartctl it
// parses -j; otherwise it parses -a text. On the megaraid path, when no
// explicit -d works it tries the common passthrough type variants.
func (s smartTool) querySmart(path, dtype string, d *Drive) bool {
args := func(extra ...string) []string {
a := []string{}
if s.jsonCapable {
a = append(a, "-j")
}
a = append(a, "-a")
if dtype != "" {
a = append(a, "-d", dtype)
}
a = append(a, extra...)
a = append(a, path)
return a
}
raw := run(s.bin, args()...)
if s.jsonCapable {
j := loadJSON(raw)
// Capture identity + transport even when SMART is unusable, so pseudo-
// device filtering can recognize SMART-less controller VDs (e.g. "DELL
// RAID") and iSCSI LUNs that expose no usable SMART.
if d.Model == "" {
d.Model = first(jStr(j, "model_name"), jStr(j, "scsi_model_name"))
}
if d.Transport == "" {
d.Transport = jStr(j, "scsi_transport_protocol", "name")
}
if jsonUsable(j) {
parseSmartJSON(j, d)
d.DevicePath = path
d.Dtype = dtype
return true
}
return false
}
// Text path: usability check is "did we get a model + some health/attrs".
if looksLikeSmartText(raw) {
parseSmartText(raw, d)
d.DevicePath = path
d.Dtype = dtype
return d.Model != "" || d.SmartHealth != "UNKNOWN"
}
return false
}
// megaraidDtypes lists the megaraid passthrough type variants to try when the
// scan didn't pin one.
var megaraidDtypes = []string{"sat+megaraid,%s", "megaraid,%s", "scsi+megaraid,%s"}
// pseudoDeviceModels lists lowercase model substrings that identify devices
// which are not physical drives: iSCSI targets and RAID controller virtual
// disks. `smartctl --scan-open` presents these as plain "-d scsi" with no
// VIRTUAL-DISK hint in the scan comment, so they are filtered after identity is
// read. Extend this list as new controller families appear in the fleet.
var pseudoDeviceModels = []string{
"virtual-disk", "virtual disk", // iSCSI IET LUNs (e.g. "IET VIRTUAL-DISK").
"lio-org", // Linux-IO iSCSI target LUNs (text-path fallback).
// RAID controller virtual disks report the HBA vendor/model as their
// identity (e.g. "AVAGO MR9363-4i", "BROADCOM MR9560-16i", "DELL PERC
// H730", "DELL RAID"). These tokens appear on controllers, never on bare
// drives.
"avago", "broadcom", "lsi", "megaraid", "perc", "adaptec", "microsemi",
"dell raid",
}
// isPseudoDevice reports whether a queried device is an iSCSI target or a RAID
// controller virtual disk rather than a physical drive. An iSCSI SCSI transport
// is the authoritative signal (covers LIO, IET, any target software); the model
// token list catches RAID virtual disks and the legacy text path that has no
// transport field.
func isPseudoDevice(d *Drive) bool {
if strings.EqualFold(d.Transport, "iSCSI") {
return true
}
m := strings.ToLower(d.Model)
if m == "" {
return false
}
for _, p := range pseudoDeviceModels {
if strings.Contains(m, p) {
return true
}
}
return false
}
// looksLikeSmartText reports whether raw is real smartctl text output worth
// parsing, keyed off identity and health section markers. It guards the text
// path from acting on error messages or empty output.
func looksLikeSmartText(raw string) bool {
if strings.TrimSpace(raw) == "" {
return false
}
for _, marker := range []string{
"=== START OF INFORMATION SECTION ===",
"Device Model:", "Model Number:", "Product:",
"SMART overall-health", "SMART Health Status",
} {
if strings.Contains(raw, marker) {
return true
}
}
return false
}