Files
scrutiny/vendor/github.com/jaypipes/ghw/block_linux.go
T
2020-08-21 06:31:48 +00:00

653 lines
18 KiB
Go

// Use and distribution licensed under the Apache license version 2.
//
// See the COPYING file in the root project directory for full text.
//
package ghw
import (
"bufio"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)
const (
sectorSize = 512
)
func (ctx *context) blockFillInfo(info *BlockInfo) error {
info.Disks = ctx.disks()
var tpb uint64
for _, d := range info.Disks {
tpb += d.SizeBytes
}
info.TotalPhysicalBytes = tpb
return nil
}
// DiskPhysicalBlockSizeBytes has been deprecated in 0.2. Please use the
// Disk.PhysicalBlockSizeBytes attribute.
// TODO(jaypipes): Remove in 1.0.
func DiskPhysicalBlockSizeBytes(disk string) uint64 {
msg := `
The DiskPhysicalBlockSizeBytes() function has been DEPRECATED and will be
removed in the 1.0 release of ghw. Please use the Disk.PhysicalBlockSizeBytes
attribute.
`
warn(msg)
ctx := contextFromEnv()
return ctx.diskPhysicalBlockSizeBytes(disk)
}
func (ctx *context) diskPhysicalBlockSizeBytes(disk string) uint64 {
// We can find the sector size in Linux by looking at the
// /sys/block/$DEVICE/queue/physical_block_size file in sysfs
path := filepath.Join(ctx.pathSysBlock(), disk, "queue", "physical_block_size")
contents, err := ioutil.ReadFile(path)
if err != nil {
return 0
}
size, err := strconv.ParseUint(strings.TrimSpace(string(contents)), 10, 64)
if err != nil {
return 0
}
return size
}
// DiskSizeBytes has been deprecated in 0.2. Please use the Disk.SizeBytes
// attribute.
// TODO(jaypipes): Remove in 1.0.
func DiskSizeBytes(disk string) uint64 {
msg := `
The DiskSizeBytes() function has been DEPRECATED and will be
removed in the 1.0 release of ghw. Please use the Disk.SizeBytes attribute.
`
warn(msg)
ctx := contextFromEnv()
return ctx.diskSizeBytes(disk)
}
func (ctx *context) diskSizeBytes(disk string) uint64 {
// We can find the number of 512-byte sectors by examining the contents of
// /sys/block/$DEVICE/size and calculate the physical bytes accordingly.
path := filepath.Join(ctx.pathSysBlock(), disk, "size")
contents, err := ioutil.ReadFile(path)
if err != nil {
return 0
}
size, err := strconv.ParseUint(strings.TrimSpace(string(contents)), 10, 64)
if err != nil {
return 0
}
return size * sectorSize
}
// DiskNUMANodeID has been deprecated in 0.2. Please use the Disk.NUMANodeID
// attribute.
// TODO(jaypipes): Remove in 1.0.
func DiskNUMANodeID(disk string) int {
msg := `
The DiskNUMANodeID() function has been DEPRECATED and will be
removed in the 1.0 release of ghw. Please use the Disk.NUMANodeID attribute.
`
warn(msg)
ctx := contextFromEnv()
return ctx.diskNUMANodeID(disk)
}
func (ctx *context) diskNUMANodeID(disk string) int {
link, err := os.Readlink(filepath.Join(ctx.pathSysBlock(), disk))
if err != nil {
return -1
}
for partial := link; strings.HasPrefix(partial, "../devices/"); partial = filepath.Base(partial) {
if nodeContents, err := ioutil.ReadFile(filepath.Join(ctx.pathSysBlock(), partial, "numa_node")); err != nil {
if nodeInt, err := strconv.Atoi(string(nodeContents)); err != nil {
return nodeInt
}
}
}
return -1
}
// DiskVendor has been deprecated in 0.2. Please use the Disk.Vendor attribute.
// TODO(jaypipes): Remove in 1.0.
func DiskVendor(disk string) string {
msg := `
The DiskVendor() function has been DEPRECATED and will be
removed in the 1.0 release of ghw. Please use the Disk.Vendor attribute.
`
warn(msg)
ctx := contextFromEnv()
return ctx.diskVendor(disk)
}
func (ctx *context) diskVendor(disk string) string {
// In Linux, the vendor for a disk device is found in the
// /sys/block/$DEVICE/device/vendor file in sysfs
path := filepath.Join(ctx.pathSysBlock(), disk, "device", "vendor")
contents, err := ioutil.ReadFile(path)
if err != nil {
return UNKNOWN
}
return strings.TrimSpace(string(contents))
}
func (ctx *context) udevInfo(disk string) (map[string]string, error) {
// Get device major:minor numbers
devNo, err := ioutil.ReadFile(filepath.Join(ctx.pathSysBlock(), disk, "dev"))
if err != nil {
return nil, err
}
// Look up block device in udev runtime database
udevID := "b" + strings.TrimSpace(string(devNo))
udevBytes, err := ioutil.ReadFile(filepath.Join(ctx.pathRunUdevData(), udevID))
if err != nil {
return nil, err
}
udevInfo := make(map[string]string)
for _, udevLine := range strings.Split(string(udevBytes), "\n") {
if strings.HasPrefix(udevLine, "E:") {
if s := strings.SplitN(udevLine[2:], "=", 2); len(s) == 2 {
udevInfo[s[0]] = s[1]
}
}
}
return udevInfo, nil
}
// DiskModel has been deprecated in 0.2. Please use the Disk.Model attribute.
// TODO(jaypipes): Remove in 1.0.
func DiskModel(disk string) string {
msg := `
The DiskModel() function has been DEPRECATED and will be removed in the 1.0
release of ghw. Please use the Disk.Model attribute.
`
warn(msg)
ctx := contextFromEnv()
return ctx.diskModel(disk)
}
func (ctx *context) diskModel(disk string) string {
info, err := ctx.udevInfo(disk)
if err != nil {
return UNKNOWN
}
if model, ok := info["ID_MODEL"]; ok {
return model
}
return UNKNOWN
}
// DiskSerialNumber has been deprecated in 0.2. Please use the Disk.SerialNumber attribute.
// TODO(jaypipes): Remove in 1.0.
func DiskSerialNumber(disk string) string {
msg := `
The DiskSerialNumber() function has been DEPRECATED and will be removed in the
1.0 release of ghw. Please use the Disk.SerialNumber attribute.
`
warn(msg)
ctx := contextFromEnv()
return ctx.diskSerialNumber(disk)
}
func (ctx *context) diskSerialNumber(disk string) string {
info, err := ctx.udevInfo(disk)
if err != nil {
return UNKNOWN
}
// There are two serial number keys, ID_SERIAL and ID_SERIAL_SHORT
// The non-_SHORT version often duplicates vendor information collected elsewhere, so use _SHORT.
if serial, ok := info["ID_SERIAL_SHORT"]; ok {
return serial
}
return UNKNOWN
}
// DiskBusPath has been deprecated in 0.2. Please use the Disk.BusPath attribute.
// TODO(jaypipes): Remove in 1.0.
func DiskBusPath(disk string) string {
msg := `
The DiskBusPath() function has been DEPRECATED and will be removed in the 1.0
release of ghw. Please use the Disk.BusPath attribute.
`
warn(msg)
ctx := contextFromEnv()
return ctx.diskBusPath(disk)
}
func (ctx *context) diskBusPath(disk string) string {
info, err := ctx.udevInfo(disk)
if err != nil {
return UNKNOWN
}
// There are two path keys, ID_PATH and ID_PATH_TAG.
// The difference seems to be _TAG has funky characters converted to underscores.
if path, ok := info["ID_PATH"]; ok {
return path
}
return UNKNOWN
}
// DiskWWN has been deprecated in 0.2. Please use the Disk.WWN attribute.
// TODO(jaypipes): Remove in 1.0.
func DiskWWN(disk string) string {
msg := `
The DiskWWN() function has been DEPRECATED and will be removed in the 1.0
release of ghw. Please use the Disk.WWN attribute.
`
warn(msg)
ctx := contextFromEnv()
return ctx.diskWWN(disk)
}
func (ctx *context) diskWWN(disk string) string {
info, err := ctx.udevInfo(disk)
if err != nil {
return UNKNOWN
}
// Trying ID_WWN_WITH_EXTENSION and falling back to ID_WWN is the same logic lsblk uses
if wwn, ok := info["ID_WWN_WITH_EXTENSION"]; ok {
return wwn
}
if wwn, ok := info["ID_WWN"]; ok {
return wwn
}
return UNKNOWN
}
// DiskPartitions has been deprecated in 0.2. Please use the Disk.Partitions attribute.
// TODO(jaypipes): Remove in 1.0.
func DiskPartitions(disk string) []*Partition {
msg := `
The DiskPartitions() function has been DEPRECATED and will be removed in the
1.0 release of ghw. Please use the Disk.Partitions attribute.
`
warn(msg)
ctx := contextFromEnv()
return ctx.diskPartitions(disk)
}
// diskPartitions takes the name of a disk (note: *not* the path of the disk,
// but just the name. In other words, "sda", not "/dev/sda" and "nvme0n1" not
// "/dev/nvme0n1") and returns a slice of pointers to Partition structs
// representing the partitions in that disk
func (ctx *context) diskPartitions(disk string) []*Partition {
out := make([]*Partition, 0)
path := filepath.Join(ctx.pathSysBlock(), disk)
files, err := ioutil.ReadDir(path)
if err != nil {
warn("failed to read disk partitions: %s\n", err)
return out
}
for _, file := range files {
fname := file.Name()
if !strings.HasPrefix(fname, disk) {
continue
}
size := ctx.partitionSizeBytes(disk, fname)
mp, pt, ro := ctx.partitionInfo(fname)
p := &Partition{
Name: fname,
SizeBytes: size,
MountPoint: mp,
Type: pt,
IsReadOnly: ro,
}
out = append(out, p)
}
return out
}
func (ctx *context) diskIsRemovable(disk string) bool {
path := filepath.Join(ctx.pathSysBlock(), disk, "removable")
contents, err := ioutil.ReadFile(path)
if err != nil {
return false
}
removable := strings.TrimSpace(string(contents))
if removable == "1" {
return true
}
return false
}
// Disks has been deprecated in 0.2. Please use the BlockInfo.Disks attribute.
// TODO(jaypipes): Remove in 1.0.
func Disks() []*Disk {
msg := `
The Disks() function has been DEPRECATED and will be removed in the
1.0 release of ghw. Please use the BlockInfo.Disks attribute.
`
warn(msg)
ctx := contextFromEnv()
return ctx.disks()
}
func (ctx *context) disks() []*Disk {
// In Linux, we could use the fdisk, lshw or blockdev commands to list disk
// information, however all of these utilities require root privileges to
// run. We can get all of this information by examining the /sys/block
// and /sys/class/block files
disks := make([]*Disk, 0)
files, err := ioutil.ReadDir(ctx.pathSysBlock())
if err != nil {
return nil
}
for _, file := range files {
dname := file.Name()
if strings.HasPrefix(dname, "loop") {
continue
}
driveType, storageController, busType := diskTypes(dname)
// TODO(jaypipes): Move this into diskTypes() once abstracting
// diskIsRotational for ease of unit testing
if !ctx.diskIsRotational(dname) {
driveType = DRIVE_TYPE_SSD
}
size := ctx.diskSizeBytes(dname)
pbs := ctx.diskPhysicalBlockSizeBytes(dname)
busPath := ctx.diskBusPath(dname)
node := ctx.diskNUMANodeID(dname)
vendor := ctx.diskVendor(dname)
model := ctx.diskModel(dname)
serialNo := ctx.diskSerialNumber(dname)
wwn := ctx.diskWWN(dname)
removable := ctx.diskIsRemovable(dname)
d := &Disk{
Name: dname,
SizeBytes: size,
PhysicalBlockSizeBytes: pbs,
DriveType: driveType,
IsRemovable: removable,
StorageController: storageController,
BusType: busType,
BusPath: busPath,
NUMANodeID: node,
Vendor: vendor,
Model: model,
SerialNumber: serialNo,
WWN: wwn,
}
parts := ctx.diskPartitions(dname)
// Map this Disk object into the Partition...
for _, part := range parts {
part.Disk = d
}
d.Partitions = parts
disks = append(disks, d)
}
return disks
}
// diskTypes returns the drive type, storage controller and bus type of a disk
func diskTypes(dname string) (
DriveType,
StorageController,
BusType,
) {
// The conditionals below which set the controller and drive type are
// based on information listed here:
// https://en.wikipedia.org/wiki/Device_file
busType := BUS_TYPE_UNKNOWN
driveType := DRIVE_TYPE_UNKNOWN
storageController := STORAGE_CONTROLLER_UNKNOWN
if strings.HasPrefix(dname, "fd") {
driveType = DRIVE_TYPE_FDD
} else if strings.HasPrefix(dname, "sd") {
driveType = DRIVE_TYPE_HDD
busType = BUS_TYPE_SCSI
storageController = STORAGE_CONTROLLER_SCSI
} else if strings.HasPrefix(dname, "hd") {
driveType = DRIVE_TYPE_HDD
busType = BUS_TYPE_IDE
storageController = STORAGE_CONTROLLER_IDE
} else if strings.HasPrefix(dname, "vd") {
driveType = DRIVE_TYPE_HDD
busType = BUS_TYPE_VIRTIO
storageController = STORAGE_CONTROLLER_VIRTIO
} else if strings.HasPrefix(dname, "nvme") {
driveType = DRIVE_TYPE_SSD
busType = BUS_TYPE_NVME
storageController = STORAGE_CONTROLLER_NVME
} else if strings.HasPrefix(dname, "sr") {
driveType = DRIVE_TYPE_ODD
busType = BUS_TYPE_SCSI
storageController = STORAGE_CONTROLLER_SCSI
} else if strings.HasPrefix(dname, "xvd") {
driveType = DRIVE_TYPE_HDD
busType = BUS_TYPE_SCSI
storageController = STORAGE_CONTROLLER_SCSI
} else if strings.HasPrefix(dname, "mmc") {
driveType = DRIVE_TYPE_SSD
busType = BUS_TYPE_UNKNOWN
storageController = STORAGE_CONTROLLER_MMC
}
return driveType, storageController, busType
}
func (ctx *context) diskIsRotational(devName string) bool {
path := filepath.Join(ctx.pathSysBlock(), devName, "queue", "rotational")
contents := safeIntFromFile(path)
return contents == 1
}
// PartitionSizeBytes has been deprecated in 0.2. Please use the
// Partition.SizeBytes attribute. TODO(jaypipes): Remove in 1.0.
func PartitionSizeBytes(part string) uint64 {
msg := `
The PartitionSizeBytes() function has been DEPRECATED and will be removed in
the 1.0 release of ghw. Please use the Partition.SizeBytes attribute.
`
warn(msg)
ctx := contextFromEnv()
disk := strings.TrimPrefix(part, "/dev")
if len(disk) < 3 {
warn("PartitionSizeBytes: unknown disk %s, returning 0\n", disk)
return 0
}
switch disk[0:2] {
case "fd":
case "sd":
case "hd":
case "vd":
case "sr":
case "mm":
disk = disk[0:3]
case "xv":
disk = disk[0:4]
case "nv":
// NOTE(jaypipes): I'm putting this regex here instead of a const
// because this function is the only thing that uses it and I'm
// deprecating this function in 1.0
// nvme0n1p2
var regexNVMeDev = regexp.MustCompile(`^nvme\d+n\d+$`)
matches := regexNVMeDev.FindSubmatch([]byte(disk))
if len(matches) < 1 {
warn("PartitionSizeBytes: unknown disk %s, returning 0\n", disk)
return 0
}
disk = string(matches[0])
}
return ctx.partitionSizeBytes(disk, part)
}
// partitionSizeBytes returns the size in bytes of the partition given a disk
// name and a partition name. Note: disk name and partition name do *not*
// contain any leading "/dev" parts. In other words, they are *names*, not
// paths.
func (ctx *context) partitionSizeBytes(disk string, part string) uint64 {
path := filepath.Join(ctx.pathSysBlock(), disk, part, "size")
contents, err := ioutil.ReadFile(path)
if err != nil {
return 0
}
size, err := strconv.ParseUint(strings.TrimSpace(string(contents)), 10, 64)
if err != nil {
return 0
}
return size * sectorSize
}
// PartitionInfo has been deprecated in 0.2. Please use the Partition struct.
// TODO(jaypipes): Remove in 1.0.
func PartitionInfo(part string) (string, string, bool) {
msg := `
The PartitionInfo() function has been DEPRECATED and will be removed in
the 1.0 release of ghw. Please use the Partition struct.
`
warn(msg)
ctx := contextFromEnv()
return ctx.partitionInfo(part)
}
// Given a full or short partition name, returns the mount point, the type of
// the partition and whether it's readonly
func (ctx *context) partitionInfo(part string) (string, string, bool) {
// Allow calling PartitionInfo with either the full partition name
// "/dev/sda1" or just "sda1"
if !strings.HasPrefix(part, "/dev") {
part = "/dev/" + part
}
// /etc/mtab entries for mounted partitions look like this:
// /dev/sda6 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
var r io.ReadCloser
r, err := os.Open(ctx.pathEtcMtab())
if err != nil {
return "", "", true
}
defer safeClose(r)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
entry := parseMtabEntry(line)
if entry == nil || entry.Partition != part {
continue
}
ro := true
for _, opt := range entry.Options {
if opt == "rw" {
ro = false
break
}
}
return entry.Mountpoint, entry.FilesystemType, ro
}
return "", "", true
}
type mtabEntry struct {
Partition string
Mountpoint string
FilesystemType string
Options []string
}
func parseMtabEntry(line string) *mtabEntry {
// /etc/mtab entries for mounted partitions look like this:
// /dev/sda6 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
if line[0] != '/' {
return nil
}
fields := strings.Fields(line)
if len(fields) < 4 {
return nil
}
// We do some special parsing of the mountpoint, which may contain space,
// tab and newline characters, encoded into the mtab entry line using their
// octal-to-string representations. From the GNU mtab man pages:
//
// "Therefore these characters are encoded in the files and the getmntent
// function takes care of the decoding while reading the entries back in.
// '\040' is used to encode a space character, '\011' to encode a tab
// character, '\012' to encode a newline character, and '\\' to encode a
// backslash."
mp := fields[1]
r := strings.NewReplacer(
"\\011", "\t", "\\012", "\n", "\\040", " ", "\\\\", "\\",
)
mp = r.Replace(mp)
res := &mtabEntry{
Partition: fields[0],
Mountpoint: mp,
FilesystemType: fields[2],
}
opts := strings.Split(fields[3], ",")
res.Options = opts
return res
}
// PartitionMountPoint has been deprecated in 0.2. Please use the
// Partition.MountPoint attribute. TODO(jaypipes): Remove in 1.0.
func PartitionMountPoint(part string) string {
msg := `
The PartitionMountPoint() function has been DEPRECATED and will be removed in
the 1.0 release of ghw. Please use the Partition.MountPoint attribute.
`
warn(msg)
ctx := contextFromEnv()
return ctx.partitionMountPoint(part)
}
func (ctx *context) partitionMountPoint(part string) string {
mp, _, _ := ctx.partitionInfo(part)
return mp
}
// PartitionType has been deprecated in 0.2. Please use the
// Partition.Type attribute. TODO(jaypipes): Remove in 1.0.
func PartitionType(part string) string {
msg := `
The PartitionType() function has been DEPRECATED and will be removed in
the 1.0 release of ghw. Please use the Partition.Type attribute.
`
warn(msg)
ctx := contextFromEnv()
return ctx.partitionType(part)
}
func (ctx *context) partitionType(part string) string {
_, pt, _ := ctx.partitionInfo(part)
return pt
}
// PartitionIsReadOnly has been deprecated in 0.2. Please use the
// Partition.IsReadOnly attribute. TODO(jaypipes): Remove in 1.0.
func PartitionIsReadOnly(part string) bool {
msg := `
The PartitionIsReadOnly() function has been DEPRECATED and will be removed in
the 1.0 release of ghw. Please use the Partition.IsReadOnly attribute.
`
warn(msg)
ctx := contextFromEnv()
return ctx.partitionIsReadOnly(part)
}
func (ctx *context) partitionIsReadOnly(part string) bool {
_, _, ro := ctx.partitionInfo(part)
return ro
}