180 lines
5.5 KiB
Go
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
|
|
}
|