github.com/jaypipes/ghw@v0.21.1/pkg/block/block_linux.go (about)

     1  // Use and distribution licensed under the Apache license version 2.
     2  //
     3  // See the COPYING file in the root project directory for full text.
     4  //
     5  
     6  package block
     7  
     8  import (
     9  	"bufio"
    10  	"io"
    11  	"os"
    12  	"path/filepath"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"github.com/jaypipes/ghw/pkg/context"
    17  	"github.com/jaypipes/ghw/pkg/linuxpath"
    18  	"github.com/jaypipes/ghw/pkg/util"
    19  )
    20  
    21  const (
    22  	sectorSize = 512
    23  )
    24  
    25  func (i *Info) load() error {
    26  	paths := linuxpath.New(i.ctx)
    27  	i.Disks = disks(i.ctx, paths)
    28  	var tsb uint64
    29  	for _, d := range i.Disks {
    30  		tsb += d.SizeBytes
    31  	}
    32  	i.TotalSizeBytes = tsb
    33  	i.TotalPhysicalBytes = tsb
    34  	return nil
    35  }
    36  
    37  func diskPhysicalBlockSizeBytes(paths *linuxpath.Paths, disk string) uint64 {
    38  	// We can find the sector size in Linux by looking at the
    39  	// /sys/block/$DEVICE/queue/physical_block_size file in sysfs
    40  	path := filepath.Join(paths.SysBlock, disk, "queue", "physical_block_size")
    41  	contents, err := os.ReadFile(path)
    42  	if err != nil {
    43  		return 0
    44  	}
    45  	size, err := strconv.ParseUint(strings.TrimSpace(string(contents)), 10, 64)
    46  	if err != nil {
    47  		return 0
    48  	}
    49  	return size
    50  }
    51  
    52  func diskSizeBytes(paths *linuxpath.Paths, disk string) uint64 {
    53  	// We can find the number of 512-byte sectors by examining the contents of
    54  	// /sys/block/$DEVICE/size and calculate the physical bytes accordingly.
    55  	path := filepath.Join(paths.SysBlock, disk, "size")
    56  	contents, err := os.ReadFile(path)
    57  	if err != nil {
    58  		return 0
    59  	}
    60  	size, err := strconv.ParseUint(strings.TrimSpace(string(contents)), 10, 64)
    61  	if err != nil {
    62  		return 0
    63  	}
    64  	return size * sectorSize
    65  }
    66  
    67  func diskNUMANodeID(paths *linuxpath.Paths, disk string) int {
    68  	link, err := os.Readlink(filepath.Join(paths.SysBlock, disk))
    69  	if err != nil {
    70  		return -1
    71  	}
    72  	for partial := link; strings.HasPrefix(partial, "../devices/"); partial = filepath.Base(partial) {
    73  		if nodeContents, err := os.ReadFile(filepath.Join(paths.SysBlock, partial, "numa_node")); err != nil {
    74  			if nodeInt, err := strconv.Atoi(string(nodeContents)); err != nil {
    75  				return nodeInt
    76  			}
    77  		}
    78  	}
    79  	return -1
    80  }
    81  
    82  func diskVendor(paths *linuxpath.Paths, disk string) string {
    83  	// In Linux, the vendor for a disk device is found in the
    84  	// /sys/block/$DEVICE/device/vendor file in sysfs
    85  	path := filepath.Join(paths.SysBlock, disk, "device", "vendor")
    86  	contents, err := os.ReadFile(path)
    87  	if err != nil {
    88  		return util.UNKNOWN
    89  	}
    90  	return strings.TrimSpace(string(contents))
    91  }
    92  
    93  // udevInfoDisk gets the udev info for a disk
    94  func udevInfoDisk(paths *linuxpath.Paths, disk string) (map[string]string, error) {
    95  	// Get device major:minor numbers
    96  	devNo, err := os.ReadFile(filepath.Join(paths.SysBlock, disk, "dev"))
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  	return udevInfo(paths, string(devNo))
   101  }
   102  
   103  // udevInfoPartition gets the udev info for a partition
   104  func udevInfoPartition(paths *linuxpath.Paths, disk string, partition string) (map[string]string, error) {
   105  	// Get device major:minor numbers
   106  	devNo, err := os.ReadFile(filepath.Join(paths.SysBlock, disk, partition, "dev"))
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	return udevInfo(paths, string(devNo))
   111  }
   112  
   113  func udevInfo(paths *linuxpath.Paths, devNo string) (map[string]string, error) {
   114  	// Look up block device in udev runtime database
   115  	udevID := "b" + strings.TrimSpace(devNo)
   116  	udevBytes, err := os.ReadFile(filepath.Join(paths.RunUdevData, udevID))
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	udevInfo := make(map[string]string)
   122  	for _, udevLine := range strings.Split(string(udevBytes), "\n") {
   123  		if strings.HasPrefix(udevLine, "E:") {
   124  			if s := strings.SplitN(udevLine[2:], "=", 2); len(s) == 2 {
   125  				udevInfo[s[0]] = s[1]
   126  			}
   127  		}
   128  	}
   129  	return udevInfo, nil
   130  }
   131  
   132  func diskModel(paths *linuxpath.Paths, disk string) string {
   133  	info, err := udevInfoDisk(paths, disk)
   134  	if err != nil {
   135  		return util.UNKNOWN
   136  	}
   137  
   138  	if model, ok := info["ID_MODEL"]; ok {
   139  		return model
   140  	}
   141  	return util.UNKNOWN
   142  }
   143  
   144  func diskSerialNumber(paths *linuxpath.Paths, disk string) string {
   145  	info, err := udevInfoDisk(paths, disk)
   146  	if err != nil {
   147  		return util.UNKNOWN
   148  	}
   149  
   150  	// First try to use the serial from sg3_utils
   151  	if serial, ok := info["SCSI_IDENT_SERIAL"]; ok {
   152  		return serial
   153  	}
   154  
   155  	// Fall back to ID_SCSI_SERIAL
   156  	if serial, ok := info["ID_SCSI_SERIAL"]; ok {
   157  		return serial
   158  	}
   159  
   160  	// There are two serial number keys, ID_SERIAL and ID_SERIAL_SHORT The
   161  	// non-_SHORT version often duplicates vendor information collected
   162  	// elsewhere, so use _SHORT and fall back to ID_SERIAL if missing...
   163  	if serial, ok := info["ID_SERIAL_SHORT"]; ok {
   164  		return serial
   165  	}
   166  	if serial, ok := info["ID_SERIAL"]; ok {
   167  		return serial
   168  	}
   169  	return util.UNKNOWN
   170  }
   171  
   172  func diskBusPath(paths *linuxpath.Paths, disk string) string {
   173  	info, err := udevInfoDisk(paths, disk)
   174  	if err != nil {
   175  		return util.UNKNOWN
   176  	}
   177  
   178  	// There are two path keys, ID_PATH and ID_PATH_TAG.
   179  	// The difference seems to be _TAG has funky characters converted to underscores.
   180  	if path, ok := info["ID_PATH"]; ok {
   181  		return path
   182  	}
   183  	return util.UNKNOWN
   184  }
   185  
   186  func diskWWNNoExtension(paths *linuxpath.Paths, disk string) string {
   187  	info, err := udevInfoDisk(paths, disk)
   188  	if err != nil {
   189  		return util.UNKNOWN
   190  	}
   191  
   192  	if wwn, ok := info["ID_WWN"]; ok {
   193  		return wwn
   194  	}
   195  	return util.UNKNOWN
   196  }
   197  
   198  func diskWWN(paths *linuxpath.Paths, disk string) string {
   199  	info, err := udevInfoDisk(paths, disk)
   200  	if err != nil {
   201  		return util.UNKNOWN
   202  	}
   203  
   204  	// Trying ID_WWN_WITH_EXTENSION and falling back to ID_WWN is the same logic lsblk uses
   205  	if wwn, ok := info["ID_WWN_WITH_EXTENSION"]; ok {
   206  		return wwn
   207  	}
   208  	if wwn, ok := info["ID_WWN"]; ok {
   209  		return wwn
   210  	}
   211  	// Device Mapper devices get DM_WWN instead of ID_WWN_WITH_EXTENSION
   212  	if wwn, ok := info["DM_WWN"]; ok {
   213  		return wwn
   214  	}
   215  	return util.UNKNOWN
   216  }
   217  
   218  // diskPartitions takes the name of a disk (note: *not* the path of the disk,
   219  // but just the name. In other words, "sda", not "/dev/sda" and "nvme0n1" not
   220  // "/dev/nvme0n1") and returns a slice of pointers to Partition structs
   221  // representing the partitions in that disk
   222  func diskPartitions(ctx *context.Context, paths *linuxpath.Paths, disk string) []*Partition {
   223  	out := make([]*Partition, 0)
   224  	path := filepath.Join(paths.SysBlock, disk)
   225  	files, err := os.ReadDir(path)
   226  	if err != nil {
   227  		ctx.Warn("failed to read disk partitions: %s\n", err)
   228  		return out
   229  	}
   230  	for _, file := range files {
   231  		fname := file.Name()
   232  		if !strings.HasPrefix(fname, disk) {
   233  			continue
   234  		}
   235  		size := partitionSizeBytes(paths, disk, fname)
   236  		mp, pt, ro := partitionInfo(paths, fname)
   237  		du := diskPartUUID(paths, disk, fname)
   238  		label := diskPartLabel(paths, disk, fname)
   239  		if pt == "" {
   240  			pt = diskPartTypeUdev(paths, disk, fname)
   241  		}
   242  		fsLabel := diskFSLabel(paths, disk, fname)
   243  		p := &Partition{
   244  			Name:            fname,
   245  			SizeBytes:       size,
   246  			MountPoint:      mp,
   247  			Type:            pt,
   248  			IsReadOnly:      ro,
   249  			UUID:            du,
   250  			Label:           label,
   251  			FilesystemLabel: fsLabel,
   252  		}
   253  		out = append(out, p)
   254  	}
   255  	return out
   256  }
   257  
   258  func diskFSLabel(paths *linuxpath.Paths, disk string, partition string) string {
   259  	info, err := udevInfoPartition(paths, disk, partition)
   260  	if err != nil {
   261  		return util.UNKNOWN
   262  	}
   263  
   264  	if label, ok := info["ID_FS_LABEL"]; ok {
   265  		return label
   266  	}
   267  	return util.UNKNOWN
   268  }
   269  
   270  func diskPartLabel(paths *linuxpath.Paths, disk string, partition string) string {
   271  	info, err := udevInfoPartition(paths, disk, partition)
   272  	if err != nil {
   273  		return util.UNKNOWN
   274  	}
   275  
   276  	if label, ok := info["ID_PART_ENTRY_NAME"]; ok {
   277  		return label
   278  	}
   279  	return util.UNKNOWN
   280  }
   281  
   282  // diskPartTypeUdev gets the partition type from the udev database directly and its only used as fallback when
   283  // the partition is not mounted, so we cannot get the type from paths.ProcMounts from the partitionInfo function
   284  func diskPartTypeUdev(paths *linuxpath.Paths, disk string, partition string) string {
   285  	info, err := udevInfoPartition(paths, disk, partition)
   286  	if err != nil {
   287  		return util.UNKNOWN
   288  	}
   289  
   290  	if pType, ok := info["ID_FS_TYPE"]; ok {
   291  		return pType
   292  	}
   293  	return util.UNKNOWN
   294  }
   295  
   296  func diskPartUUID(paths *linuxpath.Paths, disk string, partition string) string {
   297  	info, err := udevInfoPartition(paths, disk, partition)
   298  	if err != nil {
   299  		return util.UNKNOWN
   300  	}
   301  
   302  	if pType, ok := info["ID_PART_ENTRY_UUID"]; ok {
   303  		return pType
   304  	}
   305  	return util.UNKNOWN
   306  }
   307  
   308  func diskIsRemovable(paths *linuxpath.Paths, disk string) bool {
   309  	path := filepath.Join(paths.SysBlock, disk, "removable")
   310  	contents, err := os.ReadFile(path)
   311  	if err != nil {
   312  		return false
   313  	}
   314  	removable := strings.TrimSpace(string(contents))
   315  	return removable == "1"
   316  }
   317  
   318  func disks(ctx *context.Context, paths *linuxpath.Paths) []*Disk {
   319  	// In Linux, we could use the fdisk, lshw or blockdev commands to list disk
   320  	// information, however all of these utilities require root privileges to
   321  	// run. We can get all of this information by examining the /sys/block
   322  	// and /sys/class/block files
   323  	disks := make([]*Disk, 0)
   324  	files, err := os.ReadDir(paths.SysBlock)
   325  	if err != nil {
   326  		return nil
   327  	}
   328  	for _, file := range files {
   329  		dname := file.Name()
   330  
   331  		driveType, storageController := diskTypes(dname)
   332  		// TODO(jaypipes): Move this into diskTypes() once abstracting
   333  		// diskIsRotational for ease of unit testing
   334  		// Only reclassify HDD to SSD if non-rotational to avoid changing already correct types.
   335  		// This addresses changed kernel behavior where rotational detection may be unreliable,
   336  		// where some kernels report CD-ROM drives as non-rotational, incorrectly classifying them as SSD.
   337  		if !diskIsRotational(ctx, paths, dname) && driveType == DRIVE_TYPE_HDD {
   338  			driveType = DRIVE_TYPE_SSD
   339  		}
   340  		size := diskSizeBytes(paths, dname)
   341  		pbs := diskPhysicalBlockSizeBytes(paths, dname)
   342  		busPath := diskBusPath(paths, dname)
   343  		node := diskNUMANodeID(paths, dname)
   344  		vendor := diskVendor(paths, dname)
   345  		model := diskModel(paths, dname)
   346  		serialNo := diskSerialNumber(paths, dname)
   347  		wwn := diskWWN(paths, dname)
   348  		wwnNoExtension := diskWWNNoExtension(paths, dname)
   349  		removable := diskIsRemovable(paths, dname)
   350  
   351  		if storageController == STORAGE_CONTROLLER_LOOP && size == 0 {
   352  			// We don't care about unused loop devices...
   353  			continue
   354  		}
   355  		d := &Disk{
   356  			Name:                   dname,
   357  			SizeBytes:              size,
   358  			PhysicalBlockSizeBytes: pbs,
   359  			DriveType:              driveType,
   360  			IsRemovable:            removable,
   361  			StorageController:      storageController,
   362  			BusPath:                busPath,
   363  			NUMANodeID:             node,
   364  			Vendor:                 vendor,
   365  			Model:                  model,
   366  			SerialNumber:           serialNo,
   367  			WWN:                    wwn,
   368  			WWNNoExtension:         wwnNoExtension,
   369  		}
   370  
   371  		parts := diskPartitions(ctx, paths, dname)
   372  		// Map this Disk object into the Partition...
   373  		for _, part := range parts {
   374  			part.Disk = d
   375  		}
   376  		d.Partitions = parts
   377  
   378  		disks = append(disks, d)
   379  	}
   380  
   381  	return disks
   382  }
   383  
   384  // diskTypes returns the drive type, storage controller and bus type of a disk
   385  func diskTypes(dname string) (
   386  	DriveType,
   387  	StorageController,
   388  ) {
   389  	// The conditionals below which set the controller and drive type are
   390  	// based on information listed here:
   391  	// https://en.wikipedia.org/wiki/Device_file
   392  	driveType := DriveTypeUnknown
   393  	storageController := StorageControllerUnknown
   394  	if strings.HasPrefix(dname, "fd") {
   395  		driveType = DriveTypeFDD
   396  	} else if strings.HasPrefix(dname, "sd") {
   397  		driveType = DriveTypeHDD
   398  		storageController = StorageControllerSCSI
   399  	} else if strings.HasPrefix(dname, "hd") {
   400  		driveType = DriveTypeHDD
   401  		storageController = StorageControllerIDE
   402  	} else if strings.HasPrefix(dname, "vd") {
   403  		driveType = DriveTypeHDD
   404  		storageController = StorageControllerVirtIO
   405  	} else if strings.HasPrefix(dname, "nvme") {
   406  		driveType = DriveTypeSSD
   407  		storageController = StorageControllerNVMe
   408  	} else if strings.HasPrefix(dname, "sr") {
   409  		driveType = DriveTypeODD
   410  		storageController = StorageControllerSCSI
   411  	} else if strings.HasPrefix(dname, "xvd") {
   412  		driveType = DriveTypeHDD
   413  		storageController = StorageControllerSCSI
   414  	} else if strings.HasPrefix(dname, "mmc") {
   415  		driveType = DriveTypeSSD
   416  		storageController = StorageControllerMMC
   417  	} else if strings.HasPrefix(dname, "loop") {
   418  		driveType = DriveTypeVirtual
   419  		storageController = StorageControllerLoop
   420  	}
   421  
   422  	return driveType, storageController
   423  }
   424  
   425  func diskIsRotational(ctx *context.Context, paths *linuxpath.Paths, devName string) bool {
   426  	path := filepath.Join(paths.SysBlock, devName, "queue", "rotational")
   427  	contents := util.SafeIntFromFile(ctx, path)
   428  	return contents == 1
   429  }
   430  
   431  // partitionSizeBytes returns the size in bytes of the partition given a disk
   432  // name and a partition name. Note: disk name and partition name do *not*
   433  // contain any leading "/dev" parts. In other words, they are *names*, not
   434  // paths.
   435  func partitionSizeBytes(paths *linuxpath.Paths, disk string, part string) uint64 {
   436  	path := filepath.Join(paths.SysBlock, disk, part, "size")
   437  	contents, err := os.ReadFile(path)
   438  	if err != nil {
   439  		return 0
   440  	}
   441  	size, err := strconv.ParseUint(strings.TrimSpace(string(contents)), 10, 64)
   442  	if err != nil {
   443  		return 0
   444  	}
   445  	return size * sectorSize
   446  }
   447  
   448  // Given a full or short partition name, returns the mount point, the type of
   449  // the partition and whether it's readonly
   450  func partitionInfo(paths *linuxpath.Paths, part string) (string, string, bool) {
   451  	// Allow calling PartitionInfo with either the full partition name
   452  	// "/dev/sda1" or just "sda1"
   453  	if !strings.HasPrefix(part, "/dev") {
   454  		part = "/dev/" + part
   455  	}
   456  
   457  	// mount entries for mounted partitions look like this:
   458  	// /dev/sda6 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
   459  	var r io.ReadCloser
   460  	r, err := os.Open(paths.ProcMounts)
   461  	if err != nil {
   462  		return "", "", true
   463  	}
   464  	defer util.SafeClose(r)
   465  
   466  	scanner := bufio.NewScanner(r)
   467  	for scanner.Scan() {
   468  		line := scanner.Text()
   469  		entry := parseMountEntry(line)
   470  		if entry == nil || entry.Partition != part {
   471  			continue
   472  		}
   473  		ro := true
   474  		for _, opt := range entry.Options {
   475  			if opt == "rw" {
   476  				ro = false
   477  				break
   478  			}
   479  		}
   480  
   481  		return entry.Mountpoint, entry.FilesystemType, ro
   482  	}
   483  	return "", "", true
   484  }
   485  
   486  type mountEntry struct {
   487  	Partition      string
   488  	Mountpoint     string
   489  	FilesystemType string
   490  	Options        []string
   491  }
   492  
   493  func parseMountEntry(line string) *mountEntry {
   494  	// mount entries for mounted partitions look like this:
   495  	// /dev/sda6 / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
   496  	if line[0] != '/' {
   497  		return nil
   498  	}
   499  	fields := strings.Fields(line)
   500  
   501  	if len(fields) < 4 {
   502  		return nil
   503  	}
   504  
   505  	// We do some special parsing of the mountpoint, which may contain space,
   506  	// tab and newline characters, encoded into the mount entry line using their
   507  	// octal-to-string representations. From the GNU mtab man pages:
   508  	//
   509  	//   "Therefore these characters are encoded in the files and the getmntent
   510  	//   function takes care of the decoding while reading the entries back in.
   511  	//   '\040' is used to encode a space character, '\011' to encode a tab
   512  	//   character, '\012' to encode a newline character, and '\\' to encode a
   513  	//   backslash."
   514  	mp := fields[1]
   515  	r := strings.NewReplacer(
   516  		"\\011", "\t", "\\012", "\n", "\\040", " ", "\\\\", "\\",
   517  	)
   518  	mp = r.Replace(mp)
   519  
   520  	res := &mountEntry{
   521  		Partition:      fields[0],
   522  		Mountpoint:     mp,
   523  		FilesystemType: fields[2],
   524  	}
   525  	opts := strings.Split(fields[3], ",")
   526  	res.Options = opts
   527  	return res
   528  }