193 lines
4.5 KiB
Go
193 lines
4.5 KiB
Go
|
package diskio
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"bytes"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io/fs"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"strings"
|
||
|
|
||
|
"golang.org/x/sys/unix"
|
||
|
)
|
||
|
|
||
|
type diskInfoCache struct {
|
||
|
modifiedAt int64 // Unix Nano timestamp of the last modification of the device. This value is used to invalidate the cache
|
||
|
udevDataPath string
|
||
|
sysBlockPath string
|
||
|
values map[string]string
|
||
|
}
|
||
|
|
||
|
func (d *DiskIO) diskInfo(devName string) (map[string]string, error) {
|
||
|
// Check if the device exists
|
||
|
path := "/dev/" + devName
|
||
|
var stat unix.Stat_t
|
||
|
if err := unix.Stat(path, &stat); err != nil {
|
||
|
return nil, fmt.Errorf("error reading %s: %w", path, err)
|
||
|
}
|
||
|
|
||
|
// Check if we already got a cached and valid entry
|
||
|
ic, ok := d.infoCache[devName]
|
||
|
if ok && stat.Mtim.Nano() == ic.modifiedAt {
|
||
|
return ic.values, nil
|
||
|
}
|
||
|
|
||
|
// Determine udev properties
|
||
|
var udevDataPath string
|
||
|
if ok && len(ic.udevDataPath) > 0 {
|
||
|
// We can reuse the udev data path from a "previous" entry.
|
||
|
// This allows us to also "poison" it during test scenarios
|
||
|
udevDataPath = ic.udevDataPath
|
||
|
} else {
|
||
|
major := unix.Major(uint64(stat.Rdev)) //nolint:unconvert // Conversion needed for some architectures
|
||
|
minor := unix.Minor(uint64(stat.Rdev)) //nolint:unconvert // Conversion needed for some architectures
|
||
|
udevDataPath = fmt.Sprintf("/run/udev/data/b%d:%d", major, minor)
|
||
|
if _, err := os.Stat(udevDataPath); err != nil {
|
||
|
// This path failed, try the fallback .udev style (non-systemd)
|
||
|
udevDataPath = "/dev/.udev/db/block:" + devName
|
||
|
if _, err := os.Stat(udevDataPath); err != nil {
|
||
|
// Giving up, cannot retrieve disk info
|
||
|
return nil, fmt.Errorf("error reading %s: %w", udevDataPath, err)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
info, err := readUdevData(udevDataPath)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Read additional (optional) device properties
|
||
|
var sysBlockPath string
|
||
|
if ok && len(ic.sysBlockPath) > 0 {
|
||
|
// We can reuse the /sys block path from a "previous" entry.
|
||
|
// This allows us to also "poison" it during test scenarios
|
||
|
sysBlockPath = ic.sysBlockPath
|
||
|
} else {
|
||
|
sysBlockPath = "/sys/class/block/" + devName
|
||
|
}
|
||
|
|
||
|
devInfo, err := readDevData(sysBlockPath)
|
||
|
if err == nil {
|
||
|
for k, v := range devInfo {
|
||
|
info[k] = v
|
||
|
}
|
||
|
} else if !errors.Is(err, os.ErrNotExist) {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
d.infoCache[devName] = diskInfoCache{
|
||
|
modifiedAt: stat.Mtim.Nano(),
|
||
|
udevDataPath: udevDataPath,
|
||
|
values: info,
|
||
|
}
|
||
|
|
||
|
return info, nil
|
||
|
}
|
||
|
|
||
|
func readUdevData(path string) (map[string]string, error) {
|
||
|
// Final open of the confirmed (or the previously detected/used) udev file
|
||
|
f, err := os.Open(path)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer f.Close()
|
||
|
|
||
|
info := make(map[string]string)
|
||
|
scnr := bufio.NewScanner(f)
|
||
|
var devlinks bytes.Buffer
|
||
|
for scnr.Scan() {
|
||
|
l := scnr.Text()
|
||
|
if len(l) < 4 {
|
||
|
continue
|
||
|
}
|
||
|
if l[:2] == "S:" {
|
||
|
if devlinks.Len() > 0 {
|
||
|
devlinks.WriteString(" ")
|
||
|
}
|
||
|
devlinks.WriteString("/dev/")
|
||
|
devlinks.WriteString(l[2:])
|
||
|
continue
|
||
|
}
|
||
|
if l[:2] != "E:" {
|
||
|
continue
|
||
|
}
|
||
|
kv := strings.SplitN(l[2:], "=", 2)
|
||
|
if len(kv) < 2 {
|
||
|
continue
|
||
|
}
|
||
|
info[kv[0]] = kv[1]
|
||
|
}
|
||
|
|
||
|
if devlinks.Len() > 0 {
|
||
|
info["DEVLINKS"] = devlinks.String()
|
||
|
}
|
||
|
|
||
|
return info, nil
|
||
|
}
|
||
|
|
||
|
func readDevData(path string) (map[string]string, error) {
|
||
|
// Open the file and read line-wise
|
||
|
f, err := os.Open(filepath.Join(path, "uevent"))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer f.Close()
|
||
|
|
||
|
// Read DEVNAME and DEVTYPE
|
||
|
info := make(map[string]string)
|
||
|
scanner := bufio.NewScanner(f)
|
||
|
for scanner.Scan() {
|
||
|
line := scanner.Text()
|
||
|
if !strings.HasPrefix(line, "DEV") {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
k, v, found := strings.Cut(line, "=")
|
||
|
if !found {
|
||
|
continue
|
||
|
}
|
||
|
info[strings.TrimSpace(k)] = strings.TrimSpace(v)
|
||
|
}
|
||
|
if d, found := info["DEVNAME"]; found && !strings.HasPrefix(d, "/dev") {
|
||
|
info["DEVNAME"] = "/dev/" + d
|
||
|
}
|
||
|
|
||
|
// Find the DEVPATH property
|
||
|
if devlnk, err := filepath.EvalSymlinks(filepath.Join(path, "device")); err == nil {
|
||
|
devlnk = filepath.Join(devlnk, filepath.Base(path))
|
||
|
devlnk = strings.TrimPrefix(devlnk, "/sys")
|
||
|
info["DEVPATH"] = devlnk
|
||
|
}
|
||
|
|
||
|
return info, nil
|
||
|
}
|
||
|
|
||
|
func resolveName(name string) string {
|
||
|
resolved, err := filepath.EvalSymlinks(name)
|
||
|
if err == nil {
|
||
|
return resolved
|
||
|
}
|
||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||
|
return name
|
||
|
}
|
||
|
// Try to prepend "/dev"
|
||
|
resolved, err = filepath.EvalSymlinks("/dev/" + name)
|
||
|
if err != nil {
|
||
|
return name
|
||
|
}
|
||
|
|
||
|
return resolved
|
||
|
}
|
||
|
|
||
|
func getDeviceWWID(name string) string {
|
||
|
path := fmt.Sprintf("/sys/block/%s/wwid", filepath.Base(name))
|
||
|
buf, err := os.ReadFile(path)
|
||
|
if err != nil {
|
||
|
return ""
|
||
|
}
|
||
|
return strings.TrimSuffix(string(buf), "\n")
|
||
|
}
|