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 }