Skip to content

Commit

Permalink
feat: gather blockdevice information
Browse files Browse the repository at this point in the history
Gather as much as we can, use it for presentation purposes, and also for
disk matching.

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
  • Loading branch information
smira committed Jun 7, 2024
1 parent 81b69bf commit 1a51f16
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 0 deletions.
23 changes: 23 additions & 0 deletions block/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,26 @@ func (d *Device) Close() error {

// DefaultBlockSize is the default block size in bytes.
const DefaultBlockSize = 512

// DeviceProperties contains the properties of a block device.
type DeviceProperties struct {
// Device name, as in 'sda'.
DeviceName string
// Model from /sys/block/*/device/model.
Model string
// Serial /sys/block/<dev>/device/serial.
Serial string
// Modalias /sys/block/<dev>/device/modalias.
Modalias string
// WWID /sys/block/<dev>/wwid.
WWID string
// UUID /sys/block/<dev>/uuid.
// BusPath PCI bus path.
BusPath string
// SubSystem is the dest path of symlink /sys/block/<dev>/subsystem.
SubSystem string
// Transport of the device: SCSI, ata, ahci, nvme, etc.
Transport string
// Rotational is true if the device is a rotational disk.
Rotational bool
}
134 changes: 134 additions & 0 deletions block/device_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"unsafe"

"golang.org/x/sys/unix"
Expand Down Expand Up @@ -279,3 +281,135 @@ func (d *Device) lock(exclusive bool, flag int) error {
}
}
}

// GetProperties returns the properties of the block device.
func (d *Device) GetProperties() (*DeviceProperties, error) {
sysFsPath, err := d.sysFsPath()
if err != nil {
return nil, err
}

props := &DeviceProperties{
Model: readSysFsFile(filepath.Join(sysFsPath, "device", "model")),
Serial: readSysFsFile(filepath.Join(sysFsPath, "device", "serial")),
Modalias: readSysFsFile(filepath.Join(sysFsPath, "device", "modalias")),
WWID: readSysFsFile(filepath.Join(sysFsPath, "wwid")),
}

if props.WWID == "" {
props.WWID = readSysFsFile(filepath.Join(sysFsPath, "device", "wwid"))
}

fullPath, err := os.Readlink(sysFsPath)
if err == nil {
props.BusPath = filepath.Dir(filepath.Dir(strings.TrimPrefix(fullPath, "../../devices")))
props.DeviceName = filepath.Base(fullPath)
}

props.Rotational = readSysFsFile(filepath.Join(sysFsPath, "queue", "rotational")) == "1"

if subsystemPath, err := filepath.EvalSymlinks(filepath.Join(sysFsPath, "subsystem")); err == nil {
props.SubSystem = subsystemPath
}

props.Transport = d.getTransport(sysFsPath, props.DeviceName)

return props, nil
}

func (d *Device) getTransport(sysFsPath, deviceName string) string {
switch {
case strings.HasPrefix(deviceName, "nvme"):
return "nvme"
case strings.HasPrefix(deviceName, "vd"):
return "virtio"
case strings.HasPrefix(deviceName, "mmcblk"):
return "mmc"
}

devicePath, err := os.Readlink(filepath.Join(sysFsPath, "device"))
if err != nil {
return ""
}

devicePath = filepath.Base(devicePath)

hostStr, _, ok := strings.Cut(devicePath, ":")
if !ok {
return ""
}

host, err := strconv.Atoi(hostStr)
if err != nil {
return ""
}

switch {
case isScsiHost(host, "sas"):
return "sas"
case isScsiHost(host, "fc"):
return "fc"
case isScsiHost(host, "sas") && scsiHasAttribute(devicePath, "sas_device"):
return "sas"
case scsiHasAttribute(devicePath, "ieee1394_id"):
return "ibp"
case isScsiHost(host, "iscsi"):
return "iscsi"
case scsiPathContains(devicePath, "usb"):
return "usb"
case isScsiHost(host, "scsi"):
procName := readScsiHostAttribute(host, "scsi", "proc_name")

switch {
case procName == "ahci", procName == "sata":
return "sata"
case strings.Contains(procName, "ata"):
return "ata"
case procName == "virtio_scsi":
return "virtio"
}
}

return ""
}

func isScsiHost(host int, typ string) bool {
path := filepath.Join("/sys/class", typ+"_host", "host"+strconv.Itoa(host))

st, err := os.Stat(path)

return err == nil && st.IsDir()
}

func readScsiHostAttribute(host int, typ, attr string) string {
path := filepath.Join("/sys/class", typ+"_host", "host"+strconv.Itoa(host), attr)

contents, _ := os.ReadFile(path) //nolint:errcheck

return string(bytes.TrimSpace(contents))
}

func scsiHasAttribute(devicePath, attribute string) bool {
path := filepath.Join("/sys/bus/scsi/devices", devicePath, attribute)

_, err := os.Stat(path)

return err == nil
}

func scsiPathContains(devicePath, what string) bool {
path := filepath.Join("/sys/bus/scsi/devices", devicePath)

dest, _ := os.Readlink(path) //nolint:errcheck

return strings.Contains(dest, what)
}

func readSysFsFile(path string) string {
contents, err := os.ReadFile(path)
if err != nil {
return ""
}

return string(bytes.TrimSpace(contents))
}
9 changes: 9 additions & 0 deletions block/device_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,13 @@ func TestDevice(t *testing.T) {
require.NoError(t, err)
assert.True(t, readOnly)
})

t.Run("properties", func(t *testing.T) {
props, err := devWhole.GetProperties()
require.NoError(t, err)

assert.Equal(t, "/virtual", props.BusPath)
assert.Equal(t, "/sys/class/block", props.SubSystem)
assert.False(t, props.Rotational)
})
}

0 comments on commit 1a51f16

Please sign in to comment.