github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/pkg/mount/block/blockdev_linux.go (about)

     1  // Copyright 2017-2020 the u-root Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package block finds, mounts, and modifies block devices on Linux systems.
     6  package block
     7  
     8  import (
     9  	"bufio"
    10  	"encoding/binary"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"log"
    15  	"os"
    16  	"path/filepath"
    17  	"strconv"
    18  	"strings"
    19  	"unicode"
    20  	"unsafe"
    21  
    22  	"github.com/rekby/gpt"
    23  	"github.com/mvdan/u-root-coreutils/pkg/mount"
    24  	"github.com/mvdan/u-root-coreutils/pkg/pci"
    25  	"golang.org/x/sys/unix"
    26  )
    27  
    28  var (
    29  	// LinuxMountsPath is the standard mountpoint list path
    30  	LinuxMountsPath = "/proc/mounts"
    31  
    32  	// Debug function to override for verbose logging.
    33  	Debug = func(string, ...interface{}) {}
    34  
    35  	// SystemPartitionGUID is the GUID of EFI system partitions
    36  	// EFI System partitions have GUID C12A7328-F81F-11D2-BA4B-00A0C93EC93B
    37  	SystemPartitionGUID = gpt.Guid([...]byte{
    38  		0x28, 0x73, 0x2a, 0xc1,
    39  		0x1f, 0xf8,
    40  		0xd2, 0x11,
    41  		0xba, 0x4b,
    42  		0x00, 0xa0, 0xc9, 0x3e, 0xc9, 0x3b,
    43  	})
    44  )
    45  
    46  // BlockDev maps a device name to a BlockStat structure for a given block device
    47  type BlockDev struct {
    48  	Name   string
    49  	FSType string
    50  	FsUUID string
    51  }
    52  
    53  // Device makes sure the block device exists and returns a handle to it.
    54  //
    55  // maybeDevpath can be path like /dev/sda1, /sys/class/block/sda1 or just sda1.
    56  // We will just use the last component.
    57  func Device(maybeDevpath string) (*BlockDev, error) {
    58  	devname := filepath.Base(maybeDevpath)
    59  	if _, err := os.Stat(filepath.Join("/sys/class/block", devname)); err != nil {
    60  		return nil, err
    61  	}
    62  
    63  	devpath := filepath.Join("/dev/", devname)
    64  	if uuid, err := getFSUUID(devpath); err == nil {
    65  		return &BlockDev{Name: devname, FsUUID: uuid}, nil
    66  	}
    67  	return &BlockDev{Name: devname}, nil
    68  }
    69  
    70  // String implements fmt.Stringer.
    71  func (b *BlockDev) String() string {
    72  	if len(b.FSType) > 0 {
    73  		return fmt.Sprintf("BlockDevice(name=%s, fs_type=%s, fs_uuid=%s)", b.Name, b.FSType, b.FsUUID)
    74  	}
    75  	return fmt.Sprintf("BlockDevice(name=%s, fs_uuid=%s)", b.Name, b.FsUUID)
    76  }
    77  
    78  // DevicePath is the path to the actual device.
    79  func (b BlockDev) DevicePath() string {
    80  	return filepath.Join("/dev/", b.Name)
    81  }
    82  
    83  // Name implements mount.Mounter.
    84  func (b *BlockDev) DevName() string {
    85  	return b.Name
    86  }
    87  
    88  // Mount implements mount.Mounter.
    89  func (b *BlockDev) Mount(path string, flags uintptr, opts ...func() error) (*mount.MountPoint, error) {
    90  	devpath := filepath.Join("/dev", b.Name)
    91  	if len(b.FSType) > 0 {
    92  		return mount.Mount(devpath, path, b.FSType, "", flags, opts...)
    93  	}
    94  
    95  	return mount.TryMount(devpath, path, "", flags, opts...)
    96  }
    97  
    98  // GPTTable tries to read a GPT table from the block device described by the
    99  // passed BlockDev object, and returns a gpt.Table object, or an error if any
   100  func (b *BlockDev) GPTTable() (*gpt.Table, error) {
   101  	fd, err := os.Open(b.DevicePath())
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  	defer fd.Close()
   106  
   107  	blkSize, err := b.BlockSize()
   108  	if err != nil {
   109  		blkSize = 512
   110  	}
   111  
   112  	if _, err := fd.Seek(int64(blkSize), io.SeekStart); err != nil {
   113  		return nil, err
   114  	}
   115  	table, err := gpt.ReadTable(fd, uint64(blkSize))
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  	return &table, nil
   120  }
   121  
   122  // PhysicalBlockSize returns the physical block size.
   123  func (b *BlockDev) PhysicalBlockSize() (int, error) {
   124  	f, err := os.Open(b.DevicePath())
   125  	if err != nil {
   126  		return 0, err
   127  	}
   128  	defer f.Close()
   129  	return unix.IoctlGetInt(int(f.Fd()), unix.BLKPBSZGET)
   130  }
   131  
   132  // BlockSize returns the logical block size (BLKSSZGET).
   133  func (b *BlockDev) BlockSize() (int, error) {
   134  	f, err := os.Open(b.DevicePath())
   135  	if err != nil {
   136  		return 0, err
   137  	}
   138  	defer f.Close()
   139  	return unix.IoctlGetInt(int(f.Fd()), unix.BLKSSZGET)
   140  }
   141  
   142  // KernelBlockSize returns the soft block size used inside the kernel (BLKBSZGET).
   143  func (b *BlockDev) KernelBlockSize() (int, error) {
   144  	f, err := os.Open(b.DevicePath())
   145  	if err != nil {
   146  		return 0, err
   147  	}
   148  	defer f.Close()
   149  	return unix.IoctlGetInt(int(f.Fd()), unix.BLKBSZGET)
   150  }
   151  
   152  func ioctlGetUint64(fd int, req uint) (uint64, error) {
   153  	var value uint64
   154  	_, _, err := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(unsafe.Pointer(&value)))
   155  	if err != 0 {
   156  		return 0, err
   157  	}
   158  	return value, nil
   159  }
   160  
   161  // Size returns the size in bytes.
   162  func (b *BlockDev) Size() (uint64, error) {
   163  	f, err := os.Open(b.DevicePath())
   164  	if err != nil {
   165  		return 0, err
   166  	}
   167  	defer f.Close()
   168  
   169  	sz, err := ioctlGetUint64(int(f.Fd()), unix.BLKGETSIZE64)
   170  	if err != nil {
   171  		return 0, &os.PathError{
   172  			Op:   "get size",
   173  			Path: b.DevicePath(),
   174  			Err:  os.NewSyscallError("ioctl(BLKGETSIZE64)", err),
   175  		}
   176  	}
   177  	return sz, nil
   178  }
   179  
   180  // ReadPartitionTable prompts the kernel to re-read the partition table on this block device.
   181  func (b *BlockDev) ReadPartitionTable() error {
   182  	f, err := os.OpenFile(b.DevicePath(), os.O_RDWR, 0)
   183  	if err != nil {
   184  		return err
   185  	}
   186  	defer f.Close()
   187  	return unix.IoctlSetInt(int(f.Fd()), unix.BLKRRPART, 0)
   188  }
   189  
   190  // PCIInfo searches sysfs for the PCI vendor and device id.
   191  // We fill in the PCI struct with just those two elements.
   192  func (b *BlockDev) PCIInfo() (*pci.PCI, error) {
   193  	p, err := filepath.EvalSymlinks(filepath.Join("/sys/class/block", b.Name))
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  	// Loop through devices until we find the actual backing pci device.
   198  	// For Example:
   199  	// /sys/class/block/nvme0n1p1 usually resolves to something like
   200  	// /sys/devices/pci..../.../.../nvme/nvme0/nvme0n1/nvme0n1p1. This leads us to the
   201  	// first partition of the first namespace of the nvme0 device. In this case, the actual pci device and vendor
   202  	// is found in nvme, three levels up. We traverse back up to the parent device
   203  	// and we keep going until we find a device and vendor file.
   204  	dp := filepath.Join(p, "device")
   205  	vp := filepath.Join(p, "vendor")
   206  	found := false
   207  	for p != "/sys/devices" {
   208  		// Check if there is a vendor and device file in this directory.
   209  		if d, err := os.Stat(dp); err == nil && !d.IsDir() {
   210  			if v, err := os.Stat(vp); err == nil && !v.IsDir() {
   211  				found = true
   212  				break
   213  			}
   214  		}
   215  		p = filepath.Dir(p)
   216  		dp = filepath.Join(p, "device")
   217  		vp = filepath.Join(p, "vendor")
   218  	}
   219  	if !found {
   220  		return nil, fmt.Errorf("Unable to find backing pci device with device and vendor files for %v", b.Name)
   221  	}
   222  
   223  	return pci.OnePCI(p)
   224  }
   225  
   226  func getFSUUID(devpath string) (string, error) {
   227  	file, err := os.Open(devpath)
   228  	if err != nil {
   229  		return "", err
   230  	}
   231  	defer file.Close()
   232  
   233  	fsuuid, err := tryFAT32(file)
   234  	if err == nil {
   235  		return fsuuid, nil
   236  	}
   237  	fsuuid, err = tryFAT16(file)
   238  	if err == nil {
   239  		return fsuuid, nil
   240  	}
   241  	fsuuid, err = tryEXT4(file)
   242  	if err == nil {
   243  		return fsuuid, nil
   244  	}
   245  	fsuuid, err = tryXFS(file)
   246  	if err == nil {
   247  		return fsuuid, nil
   248  	}
   249  	return "", fmt.Errorf("unknown UUID (not vfat, ext4, nor xfs)")
   250  }
   251  
   252  // See https://www.nongnu.org/ext2-doc/ext2.html#DISK-ORGANISATION.
   253  const (
   254  	// Offset of superblock in partition.
   255  	ext2SprblkOff = 1024
   256  
   257  	// Offset of magic number in suberblock.
   258  	ext2SprblkMagicOff  = 56
   259  	ext2SprblkMagicSize = 2
   260  
   261  	ext2SprblkMagic = 0xEF53
   262  
   263  	// Offset of UUID in superblock.
   264  	ext2SprblkUUIDOff  = 104
   265  	ext2SprblkUUIDSize = 16
   266  )
   267  
   268  func tryEXT4(file io.ReaderAt) (string, error) {
   269  	var off int64
   270  
   271  	// Read magic number.
   272  	b := make([]byte, ext2SprblkMagicSize)
   273  	off = ext2SprblkOff + ext2SprblkMagicOff
   274  	if _, err := file.ReadAt(b, off); err != nil {
   275  		return "", err
   276  	}
   277  	magic := binary.LittleEndian.Uint16(b[:2])
   278  	if magic != ext2SprblkMagic {
   279  		return "", fmt.Errorf("ext4 magic not found")
   280  	}
   281  
   282  	// Filesystem UUID.
   283  	b = make([]byte, ext2SprblkUUIDSize)
   284  	off = ext2SprblkOff + ext2SprblkUUIDOff
   285  	if _, err := file.ReadAt(b, off); err != nil {
   286  		return "", err
   287  	}
   288  
   289  	return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]), nil
   290  }
   291  
   292  // See https://de.wikipedia.org/wiki/File_Allocation_Table#Aufbau.
   293  const (
   294  	fat12Magic = "FAT12   "
   295  	fat16Magic = "FAT16   "
   296  
   297  	// Offset of magic number.
   298  	fat16MagicOff  = 0x36
   299  	fat16MagicSize = 8
   300  
   301  	// Offset of filesystem ID / serial number. Treated as short filesystem UUID.
   302  	fat16IDOff  = 0x27
   303  	fat16IDSize = 4
   304  )
   305  
   306  func tryFAT16(file io.ReaderAt) (string, error) {
   307  	// Read magic number.
   308  	b := make([]byte, fat16MagicSize)
   309  	if _, err := file.ReadAt(b, fat16MagicOff); err != nil {
   310  		return "", err
   311  	}
   312  	magic := string(b)
   313  	if magic != fat16Magic && magic != fat12Magic {
   314  		return "", fmt.Errorf("fat16 magic not found")
   315  	}
   316  
   317  	// Filesystem UUID.
   318  	b = make([]byte, fat16IDSize)
   319  	if _, err := file.ReadAt(b, fat16IDOff); err != nil {
   320  		return "", err
   321  	}
   322  
   323  	return fmt.Sprintf("%02x%02x-%02x%02x", b[3], b[2], b[1], b[0]), nil
   324  }
   325  
   326  // See https://de.wikipedia.org/wiki/File_Allocation_Table#Aufbau.
   327  const (
   328  	fat32Magic = "FAT32   "
   329  
   330  	// Offset of magic number.
   331  	fat32MagicOff  = 0x52
   332  	fat32MagicSize = 8
   333  
   334  	// Offset of filesystem ID / serial number. Treated as short filesystem UUID.
   335  	fat32IDOff  = 67
   336  	fat32IDSize = 4
   337  )
   338  
   339  func tryFAT32(file io.ReaderAt) (string, error) {
   340  	// Read magic number.
   341  	b := make([]byte, fat32MagicSize)
   342  	if _, err := file.ReadAt(b, fat32MagicOff); err != nil {
   343  		return "", err
   344  	}
   345  	magic := string(b)
   346  	if magic != fat32Magic {
   347  		return "", fmt.Errorf("fat32 magic not found")
   348  	}
   349  
   350  	// Filesystem UUID.
   351  	b = make([]byte, fat32IDSize)
   352  	if _, err := file.ReadAt(b, fat32IDOff); err != nil {
   353  		return "", err
   354  	}
   355  
   356  	return fmt.Sprintf("%02x%02x-%02x%02x", b[3], b[2], b[1], b[0]), nil
   357  }
   358  
   359  const (
   360  	xfsMagic     = "XFSB"
   361  	xfsMagicSize = 4
   362  	xfsUUIDOff   = 32
   363  	xfsUUIDSize  = 16
   364  )
   365  
   366  func tryXFS(file io.ReaderAt) (string, error) {
   367  	// Read magic number.
   368  	b := make([]byte, xfsMagicSize)
   369  	if _, err := file.ReadAt(b, 0); err != nil {
   370  		return "", err
   371  	}
   372  	magic := string(b)
   373  	if magic != xfsMagic {
   374  		return "", fmt.Errorf("xfs magic not found")
   375  	}
   376  
   377  	// Filesystem UUID.
   378  	b = make([]byte, xfsUUIDSize)
   379  	if _, err := file.ReadAt(b, xfsUUIDOff); err != nil {
   380  		return "", err
   381  	}
   382  
   383  	return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]), nil
   384  }
   385  
   386  // BlockDevices is a list of block devices.
   387  type BlockDevices []*BlockDev
   388  
   389  // GetBlockDevices iterates over /sys/class/block entries and returns a list of
   390  // BlockDev objects, or an error if any
   391  func GetBlockDevices() (BlockDevices, error) {
   392  	var blockdevs []*BlockDev
   393  	var devnames []string
   394  
   395  	root := "/sys/class/block"
   396  	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
   397  		if err != nil {
   398  			return err
   399  		}
   400  		rel, err := filepath.Rel(root, path)
   401  		if err != nil {
   402  			return err
   403  		}
   404  		if rel == "." {
   405  			return nil
   406  		}
   407  		devnames = append(devnames, rel)
   408  		dev, err := Device(rel)
   409  		if err != nil {
   410  			return err
   411  		}
   412  		blockdevs = append(blockdevs, dev)
   413  		return nil
   414  	})
   415  	if err != nil {
   416  		return nil, err
   417  	}
   418  	return blockdevs, nil
   419  }
   420  
   421  // FilterName returns a list of BlockDev objects whose underlying
   422  // block device has a Name with the given Name
   423  func (b BlockDevices) FilterName(name string) BlockDevices {
   424  	partitions := make(BlockDevices, 0)
   425  	for _, device := range b {
   426  		if device.Name == name {
   427  			partitions = append(partitions, device)
   428  		}
   429  	}
   430  	return partitions
   431  }
   432  
   433  // FilterNames filters block devices by the given list of device names (e.g.
   434  // /dev/sda1 sda2 /sys/class/block/sda3).
   435  func (b BlockDevices) FilterNames(names ...string) BlockDevices {
   436  	m := make(map[string]struct{})
   437  	for _, n := range names {
   438  		m[filepath.Base(n)] = struct{}{}
   439  	}
   440  
   441  	var devices BlockDevices
   442  	for _, device := range b {
   443  		if _, ok := m[device.Name]; ok {
   444  			devices = append(devices, device)
   445  		}
   446  	}
   447  	return devices
   448  }
   449  
   450  // FilterFSUUID returns a list of BlockDev objects whose underlying block
   451  // device has a filesystem with the given FSUUID.
   452  func (b BlockDevices) FilterFSUUID(fsuuid string) BlockDevices {
   453  	partitions := make(BlockDevices, 0)
   454  	for _, device := range b {
   455  		if device.FsUUID == fsuuid {
   456  			partitions = append(partitions, device)
   457  		}
   458  	}
   459  	return partitions
   460  }
   461  
   462  // FilterZeroSize attempts to find block devices that have at least one block
   463  // of content.
   464  //
   465  // This serves to eliminate block devices that have no backing storage, but
   466  // appear in /sys/class/block anyway (like some loop, nbd, or ram devices).
   467  func (b BlockDevices) FilterZeroSize() BlockDevices {
   468  	var nb BlockDevices
   469  	for _, device := range b {
   470  		if n, err := device.Size(); err != nil || n == 0 {
   471  			continue
   472  		}
   473  		nb = append(nb, device)
   474  	}
   475  	return nb
   476  }
   477  
   478  // FilterHavingPartitions returns BlockDevices with have the specified
   479  // partitions. (e.g. f(1, 2) {sda, sda1, sda2, sdb} -> {sda})
   480  func (b BlockDevices) FilterHavingPartitions(parts []int) BlockDevices {
   481  	devices := make(BlockDevices, 0)
   482  	for _, device := range b {
   483  		hasParts := true
   484  		for _, part := range parts {
   485  			if _, err := os.Stat(filepath.Join("/sys/class/block",
   486  				ComposePartName(device.Name, part))); err != nil {
   487  				hasParts = false
   488  				break
   489  			}
   490  		}
   491  		if hasParts {
   492  			devices = append(devices, device)
   493  		}
   494  	}
   495  	return devices
   496  }
   497  
   498  // FilterPartID returns partitions with the given partition ID GUID.
   499  func (b BlockDevices) FilterPartID(guid string) BlockDevices {
   500  	var names []string
   501  	for _, device := range b {
   502  		table, err := device.GPTTable()
   503  		if err != nil {
   504  			continue
   505  		}
   506  		for i, part := range table.Partitions {
   507  			if part.IsEmpty() {
   508  				continue
   509  			}
   510  			if strings.EqualFold(part.Id.String(), guid) {
   511  				names = append(names, ComposePartName(device.Name, i+1))
   512  			}
   513  		}
   514  	}
   515  	return b.FilterNames(names...)
   516  }
   517  
   518  // FilterPartType returns partitions with the given partition type GUID.
   519  func (b BlockDevices) FilterPartType(guid string) BlockDevices {
   520  	var names []string
   521  	for _, device := range b {
   522  		table, err := device.GPTTable()
   523  		if err != nil {
   524  			continue
   525  		}
   526  		for i, part := range table.Partitions {
   527  			if part.IsEmpty() {
   528  				continue
   529  			}
   530  			if strings.EqualFold(part.Type.String(), guid) {
   531  				names = append(names, ComposePartName(device.Name, i+1))
   532  			}
   533  		}
   534  	}
   535  	return b.FilterNames(names...)
   536  }
   537  
   538  // FilterPartLabel returns a list of BlockDev objects whose underlying block
   539  // device has the given partition label. The name comparison is case-insensitive.
   540  func (b BlockDevices) FilterPartLabel(label string) BlockDevices {
   541  	var names []string
   542  	for _, device := range b {
   543  		table, err := device.GPTTable()
   544  		if err != nil {
   545  			continue
   546  		}
   547  		for i, part := range table.Partitions {
   548  			if part.IsEmpty() {
   549  				continue
   550  			}
   551  			if strings.EqualFold(part.Name(), label) {
   552  				names = append(names, ComposePartName(device.Name, i+1))
   553  			}
   554  		}
   555  	}
   556  	return b.FilterNames(names...)
   557  }
   558  
   559  // FilterBlockPCIString parses a string in the format vendor:device,vendor:device
   560  // and returns a list of BlockDev objects whose backing pci devices do not match
   561  // the vendor:device pairs passed in. All values are treated as hex.
   562  // E.g. 0x8086:0xABCD,8086:0x1234
   563  func (b BlockDevices) FilterBlockPCIString(blocklist string) (BlockDevices, error) {
   564  	pciList, err := parsePCIBlockList(blocklist)
   565  	if err != nil {
   566  		return nil, err
   567  	}
   568  	return b.FilterBlockPCI(pciList), nil
   569  }
   570  
   571  // FilterBlockPCI returns a list of BlockDev objects whose backing
   572  // pci devices do not match the blocklist of PCI devices passed in.
   573  // FilterBlockPCI discards entries which have a matching PCI vendor
   574  // and device ID as an entry in the blocklist.
   575  func (b BlockDevices) FilterBlockPCI(blocklist pci.Devices) BlockDevices {
   576  	type mapKey struct {
   577  		vendor, device uint16
   578  	}
   579  	m := make(map[mapKey]bool)
   580  
   581  	for _, v := range blocklist {
   582  		m[mapKey{v.Vendor, v.Device}] = true
   583  	}
   584  	Debug("block map is %v", m)
   585  
   586  	partitions := make(BlockDevices, 0)
   587  	for _, device := range b {
   588  		p, err := device.PCIInfo()
   589  		if err != nil {
   590  			// In the case of an error, we err on the safe side and choose not to block it.
   591  			// Not all block devices are backed by a pci device, for example SATA drives.
   592  			Debug("Failed to find PCI info; %v", err)
   593  			partitions = append(partitions, device)
   594  			continue
   595  		}
   596  		if _, ok := m[mapKey{p.Vendor, p.Device}]; !ok {
   597  			// Not in blocklist, we're good to go
   598  			Debug("Not blocking device %v, with pci %v, not in map", device, p)
   599  			partitions = append(partitions, device)
   600  		} else {
   601  			log.Printf("Blocking device %v since it appears in blocklist", device.Name)
   602  		}
   603  	}
   604  	return partitions
   605  }
   606  
   607  // parsePCIBlockList parses a string in the format vendor:device,vendor:device
   608  // and returns a list of PCI devices containing the vendor and device pairs to block.
   609  func parsePCIBlockList(blockList string) (pci.Devices, error) {
   610  	pciList := pci.Devices{}
   611  	bL := strings.Split(blockList, ",")
   612  	for _, b := range bL {
   613  		p := strings.Split(b, ":")
   614  		if len(p) != 2 {
   615  			return nil, fmt.Errorf("BlockList needs to be of format vendor1:device1,vendor2:device2...! got %v", blockList)
   616  		}
   617  		// Check that values are hex and convert them to sysfs formats
   618  		// This accepts 0xABCD and turns it into 0xabcd
   619  		// abcd also turns into 0xabcd
   620  		v, err := strconv.ParseUint(strings.TrimPrefix(p[0], "0x"), 16, 16)
   621  		if err != nil {
   622  			return nil, fmt.Errorf("BlockList needs to contain a hex vendor ID, got %v, err %v", p[0], err)
   623  		}
   624  
   625  		d, err := strconv.ParseUint(strings.TrimPrefix(p[1], "0x"), 16, 16)
   626  		if err != nil {
   627  			return nil, fmt.Errorf("BlockList needs to contain a hex device ID, got %v, err %v", p[1], err)
   628  		}
   629  
   630  		pciList = append(pciList, &pci.PCI{Vendor: uint16(v), Device: uint16(d)})
   631  	}
   632  	return pciList, nil
   633  }
   634  
   635  // ComposePartName returns the partition name described by the parent devName
   636  // and partNo counting from 1. It is assumed that device names ending in a
   637  // number like nvme0n1 have partitions named like nvme0n1p1, nvme0n1p2, ...
   638  // and devices ending in a letter like sda have partitions named like
   639  //
   640  //	sda1, sda2, ...
   641  func ComposePartName(devName string, partNo int) string {
   642  	r := []rune(devName[len(devName)-1:])
   643  	if unicode.IsDigit(r[0]) {
   644  		return fmt.Sprintf("%sp%d", devName, partNo)
   645  	}
   646  	return fmt.Sprintf("%s%d", devName, partNo)
   647  }
   648  
   649  // GetMountpointByDevice gets the mountpoint by given
   650  // device name. Returns on first match
   651  func GetMountpointByDevice(devicePath string) (*string, error) {
   652  	file, err := os.Open(LinuxMountsPath)
   653  	if err != nil {
   654  		return nil, err
   655  	}
   656  
   657  	defer file.Close()
   658  	scanner := bufio.NewScanner(file)
   659  
   660  	for scanner.Scan() {
   661  		deviceInfo := strings.Fields(scanner.Text())
   662  		if deviceInfo[0] == devicePath {
   663  			return &deviceInfo[1], nil
   664  		}
   665  	}
   666  
   667  	return nil, errors.New("Mountpoint not found")
   668  }