Disks: one card per physical drive (NVMe namespaces folded under their controller), partitions summarised on one line instead of listed

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-18 14:18:20 -07:00
co-authored by Claude Fable 5.1
parent a372378098
commit 0af24f41c3
4 changed files with 112 additions and 23 deletions
+95 -12
View File
@@ -35,17 +35,18 @@ type Part struct {
}
type Device struct {
Name string `json:"name"`
Kind string `json:"kind"` // nvme | ssd | hdd | usb | other
Model string `json:"model,omitempty"`
Vendor string `json:"vendor,omitempty"`
Serial string `json:"serial,omitempty"`
Tran string `json:"tran,omitempty"`
Size uint64 `json:"size"`
Rotational bool `json:"rotational"`
Removable bool `json:"removable"`
Temp float64 `json:"temp,omitempty"`
Parts []Part `json:"parts"`
Name string `json:"name"`
Kind string `json:"kind"` // nvme | ssd | hdd | usb | other
Model string `json:"model,omitempty"`
Vendor string `json:"vendor,omitempty"`
Serial string `json:"serial,omitempty"`
Tran string `json:"tran,omitempty"`
Size uint64 `json:"size"`
Rotational bool `json:"rotational"`
Removable bool `json:"removable"`
Temp float64 `json:"temp,omitempty"`
Parts []Part `json:"parts"`
Namespaces []string `json:"namespaces,omitempty"` // NVMe: the block devices behind this controller
}
type lsblkNode struct {
@@ -121,10 +122,89 @@ func Devices(ctx context.Context) ([]Device, error) {
walk(n.Children)
devs = append(devs, d)
}
devs = foldNVMe(devs)
sort.Slice(devs, func(i, j int) bool { return devs[i].Name < devs[j].Name })
return devs, nil
}
var nsRe = regexp.MustCompile(`^(nvme\d+)n\d+$`)
// foldNVMe merges nvme0n1, nvme0n2, … into one physical device "nvme0": one SSD, one
// SMART record, however many namespaces the controller presents (Apple exposes three).
func foldNVMe(devs []Device) []Device {
byCtrl := map[string]*Device{}
var out []Device
for _, d := range devs {
m := nsRe.FindStringSubmatch(d.Name)
if m == nil {
out = append(out, d)
continue
}
ctrl := m[1]
c, ok := byCtrl[ctrl]
if !ok {
nd := d
nd.Name = ctrl
nd.Parts = []Part{}
nd.Namespaces = nil
if mdl := strings.TrimSpace(readStrFile("/sys/class/nvme/" + ctrl + "/model")); mdl != "" {
nd.Model = mdl
}
if sn := strings.TrimSpace(readStrFile("/sys/class/nvme/" + ctrl + "/serial")); sn != "" {
nd.Serial = sn
}
if t := ctrlTemp(ctrl); t > 0 {
nd.Temp = t
}
nd.Size = 0
c = &nd
byCtrl[ctrl] = c
out = append(out, Device{}) // placeholder keeps order; filled below
c.Namespaces = []string{}
}
c.Size += d.Size
c.Namespaces = append(c.Namespaces, d.Name)
c.Parts = append(c.Parts, d.Parts...)
}
// replace placeholders with the folded controllers, in first-seen order
res := out[:0]
seen := map[string]bool{}
for _, d := range out {
if d.Name != "" {
res = append(res, d)
continue
}
for _, c := range byCtrl {
if !seen[c.Name] {
seen[c.Name] = true
res = append(res, *c)
break
}
}
}
return res
}
func readStrFile(p string) string {
b, err := os.ReadFile(p)
if err != nil {
return ""
}
return string(b)
}
func ctrlTemp(ctrl string) float64 {
dirs, _ := filepath.Glob("/sys/class/nvme/" + ctrl + "/hwmon*")
for _, d := range dirs {
if b, err := os.ReadFile(filepath.Join(d, "temp1_input")); err == nil {
if v, err := strconv.Atoi(strings.TrimSpace(string(b))); err == nil {
return float64(v) / 1000
}
}
}
return 0
}
func blockTemp(name string) float64 {
for _, pat := range []string{"/sys/block/" + name + "/device/hwmon*", "/sys/block/" + name + "/device/hwmon/hwmon*"} {
dirs, _ := filepath.Glob(pat)
@@ -145,7 +225,10 @@ func validDevice(name string) bool {
if !devRe.MatchString(name) {
return false
}
_, err := os.Stat("/sys/block/" + name)
if _, err := os.Stat("/sys/block/" + name); err == nil {
return true
}
_, err := os.Stat("/sys/class/nvme/" + name) // NVMe controller (nvme0), which is what smartctl wants anyway
return err == nil
}