github.com/xyproto/u-root@v6.0.1-0.20200302025726-5528e0c77a3c+incompatible/pkg/storage/blockdev.go (about)

     1  // Copyright 2017-2019 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 storage
     6  
     7  import (
     8  	"bufio"
     9  	"encoding/binary"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"io/ioutil"
    14  	"log"
    15  	"os"
    16  	"path"
    17  	"path/filepath"
    18  	"strconv"
    19  	"strings"
    20  
    21  	"github.com/rekby/gpt"
    22  	"github.com/u-root/u-root/pkg/mount"
    23  )
    24  
    25  var (
    26  	// LinuxMountsPath is the standard mountpoint list path
    27  	LinuxMountsPath = "/proc/mounts"
    28  )
    29  
    30  // BlockDev maps a device name to a BlockStat structure for a given block device
    31  type BlockDev struct {
    32  	Name   string
    33  	FSType string
    34  	Stat   BlockStat
    35  	FsUUID string
    36  }
    37  
    38  // String implements fmt.Stringer.
    39  func (b BlockDev) String() string {
    40  	return fmt.Sprintf("BlockDevice(name=%s, fs_uuid=%s)", b.Name, b.FsUUID)
    41  }
    42  
    43  // Mount implements mount.Mounter.
    44  func (b BlockDev) Mount(path string, flags uintptr) (*mount.MountPoint, error) {
    45  	devpath := filepath.Join("/dev", b.Name)
    46  	if len(b.FSType) > 0 {
    47  		return mount.Mount(devpath, path, b.FSType, "", flags)
    48  	}
    49  
    50  	return mount.TryMount(devpath, path, flags)
    51  }
    52  
    53  // Summary prints a multiline summary of the BlockDev object
    54  // https://www.kernel.org/doc/Documentation/block/stat.txt
    55  func (b BlockDev) Summary() string {
    56  	return fmt.Sprintf(`BlockStat{
    57      Name: %v
    58      ReadIOs: %v
    59      ReadMerges: %v
    60      ReadSectors: %v
    61      ReadTicks: %v
    62      WriteIOs: %v
    63      WriteMerges: %v
    64      WriteSectors: %v
    65      WriteTicks: %v
    66      InFlight: %v
    67      IOTicks: %v
    68      TimeInQueue: %v
    69  }`,
    70  		b.Name,
    71  		b.Stat.ReadIOs,
    72  		b.Stat.ReadMerges,
    73  		b.Stat.ReadSectors,
    74  		b.Stat.ReadTicks,
    75  		b.Stat.WriteIOs,
    76  		b.Stat.WriteMerges,
    77  		b.Stat.WriteSectors,
    78  		b.Stat.WriteTicks,
    79  		b.Stat.InFlight,
    80  		b.Stat.IOTicks,
    81  		b.Stat.TimeInQueue,
    82  	)
    83  }
    84  
    85  // BlockStat provides block device information as contained in
    86  // /sys/class/block/<device_name>/stat
    87  type BlockStat struct {
    88  	ReadIOs      uint64
    89  	ReadMerges   uint64
    90  	ReadSectors  uint64
    91  	ReadTicks    uint64
    92  	WriteIOs     uint64
    93  	WriteMerges  uint64
    94  	WriteSectors uint64
    95  	WriteTicks   uint64
    96  	InFlight     uint64
    97  	IOTicks      uint64
    98  	TimeInQueue  uint64
    99  	// Kernel 4.18 added four fields for discard tracking, see
   100  	// https://github.com/torvalds/linux/commit/bdca3c87fb7ad1cc61d231d37eb0d8f90d001e0c
   101  	DiscardIOs     uint64
   102  	DiscardMerges  uint64
   103  	DiscardSectors uint64
   104  	DiscardTicks   uint64
   105  }
   106  
   107  // SystemPartitionGUID is the GUID of EFI system partitions
   108  // EFI System partitions have GUID C12A7328-F81F-11D2-BA4B-00A0C93EC93B
   109  var SystemPartitionGUID = gpt.Guid([...]byte{
   110  	0x28, 0x73, 0x2a, 0xc1,
   111  	0x1f, 0xf8,
   112  	0xd2, 0x11,
   113  	0xba, 0x4b,
   114  	0x00, 0xa0, 0xc9, 0x3e, 0xc9, 0x3b,
   115  })
   116  
   117  // BlockStatFromBytes parses a block stat file and returns a BlockStat object.
   118  // The format of the block stat file is the one defined by Linux for
   119  // /sys/class/block/<device_name>/stat
   120  func BlockStatFromBytes(buf []byte) (*BlockStat, error) {
   121  	fields := strings.Fields(string(buf))
   122  	// BlockStat has 11 fields
   123  	if len(fields) < 11 {
   124  		return nil, fmt.Errorf("BlockStatFromBytes: parsing %q: got %d fields(%q), want at least 11", buf, len(fields), fields)
   125  	}
   126  	intfields := make([]uint64, 0)
   127  	for _, field := range fields {
   128  		v, err := strconv.ParseUint(field, 10, 64)
   129  		if err != nil {
   130  			return nil, err
   131  		}
   132  		intfields = append(intfields, v)
   133  	}
   134  	bs := BlockStat{
   135  		ReadIOs:      intfields[0],
   136  		ReadMerges:   intfields[1],
   137  		ReadSectors:  intfields[2],
   138  		ReadTicks:    intfields[3],
   139  		WriteIOs:     intfields[4],
   140  		WriteMerges:  intfields[5],
   141  		WriteSectors: intfields[6],
   142  		WriteTicks:   intfields[7],
   143  		InFlight:     intfields[8],
   144  		IOTicks:      intfields[9],
   145  		TimeInQueue:  intfields[10],
   146  	}
   147  	if len(fields) >= 15 {
   148  		bs.DiscardIOs = intfields[11]
   149  		bs.DiscardMerges = intfields[12]
   150  		bs.DiscardSectors = intfields[13]
   151  		bs.DiscardTicks = intfields[14]
   152  	}
   153  	return &bs, nil
   154  }
   155  
   156  // GetBlockStats iterates over /sys/class/block entries and returns a list of
   157  // BlockDev objects, or an error if any
   158  func GetBlockStats() ([]BlockDev, error) {
   159  	blockdevs := make([]BlockDev, 0)
   160  	devnames := make([]string, 0)
   161  	root := "/sys/class/block"
   162  	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
   163  		if err != nil {
   164  			return err
   165  		}
   166  		rel, err := filepath.Rel(root, path)
   167  		if err != nil {
   168  			return err
   169  		}
   170  		if rel == "." {
   171  			return nil
   172  		}
   173  		devnames = append(devnames, rel)
   174  		return nil
   175  	})
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  	for _, devname := range devnames {
   180  		buf, err := ioutil.ReadFile(fmt.Sprintf("%s/%s/stat", root, devname))
   181  		if err != nil {
   182  			return nil, err
   183  		}
   184  		bstat, err := BlockStatFromBytes(buf)
   185  		if err != nil {
   186  			return nil, err
   187  		}
   188  		devpath := path.Join("/dev/", devname)
   189  		if uuid, err := getUUID(devpath); err != nil {
   190  			blockdevs = append(blockdevs, BlockDev{Name: devname, Stat: *bstat})
   191  		} else {
   192  			blockdevs = append(blockdevs, BlockDev{Name: devname, Stat: *bstat, FsUUID: uuid})
   193  		}
   194  	}
   195  	return blockdevs, nil
   196  }
   197  
   198  func getUUID(devpath string) (string, error) {
   199  	file, err := os.Open(devpath)
   200  	if err != nil {
   201  		return "", err
   202  	}
   203  	defer file.Close()
   204  
   205  	fsuuid, err := tryVFAT(file)
   206  	if err == nil {
   207  		return fsuuid, nil
   208  	}
   209  	fsuuid, err = tryEXT4(file)
   210  	if err == nil {
   211  		return fsuuid, nil
   212  	}
   213  	fsuuid, err = tryXFS(file)
   214  	if err == nil {
   215  		return fsuuid, nil
   216  	}
   217  	return "", fmt.Errorf("unknown UUID (not vfat, ext4, nor xfs)")
   218  }
   219  
   220  // See https://www.nongnu.org/ext2-doc/ext2.html#DISK-ORGANISATION.
   221  const (
   222  	// Offset of superblock in partition.
   223  	ext2SprblkOff = 1024
   224  
   225  	// Offset of magic number in suberblock.
   226  	ext2SprblkMagicOff  = 56
   227  	ext2SprblkMagicSize = 2
   228  
   229  	ext2SprblkMagic = 0xEF53
   230  
   231  	// Offset of UUID in superblock.
   232  	ext2SprblkUUIDOff  = 104
   233  	ext2SprblkUUIDSize = 16
   234  )
   235  
   236  func tryEXT4(file io.ReaderAt) (string, error) {
   237  	var off int64
   238  
   239  	// Read magic number.
   240  	b := make([]byte, ext2SprblkMagicSize)
   241  	off = ext2SprblkOff + ext2SprblkMagicOff
   242  	if _, err := file.ReadAt(b, off); err != nil {
   243  		return "", err
   244  	}
   245  	magic := binary.LittleEndian.Uint16(b[:2])
   246  	if magic != ext2SprblkMagic {
   247  		return "", fmt.Errorf("ext4 magic not found")
   248  	}
   249  
   250  	// Filesystem UUID.
   251  	b = make([]byte, ext2SprblkUUIDSize)
   252  	off = ext2SprblkOff + ext2SprblkUUIDOff
   253  	if _, err := file.ReadAt(b, off); err != nil {
   254  		return "", err
   255  	}
   256  
   257  	return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]), nil
   258  }
   259  
   260  // See https://de.wikipedia.org/wiki/File_Allocation_Table#Aufbau.
   261  const (
   262  	fat32Magic = "FAT32   "
   263  
   264  	// Offset of magic number.
   265  	fat32MagicOff  = 82
   266  	fat32MagicSize = 8
   267  
   268  	// Offset of filesystem ID / serial number. Treated as short filesystem UUID.
   269  	fat32IDOff  = 67
   270  	fat32IDSize = 4
   271  )
   272  
   273  func tryVFAT(file io.ReaderAt) (string, error) {
   274  	// Read magic number.
   275  	b := make([]byte, fat32MagicSize)
   276  	if _, err := file.ReadAt(b, fat32MagicOff); err != nil {
   277  		return "", err
   278  	}
   279  	magic := string(b)
   280  	if magic != fat32Magic {
   281  		return "", fmt.Errorf("fat32 magic not found")
   282  	}
   283  
   284  	// Filesystem UUID.
   285  	b = make([]byte, fat32IDSize)
   286  	if _, err := file.ReadAt(b, fat32IDOff); err != nil {
   287  		return "", err
   288  	}
   289  
   290  	return fmt.Sprintf("%02x%02x-%02x%02x", b[3], b[2], b[1], b[0]), nil
   291  }
   292  
   293  const (
   294  	xfsMagic     = "XFSB"
   295  	xfsMagicSize = 4
   296  	xfsUUIDOff   = 32
   297  	xfsUUIDSize  = 16
   298  )
   299  
   300  func tryXFS(file io.ReaderAt) (string, error) {
   301  	// Read magic number.
   302  	b := make([]byte, xfsMagicSize)
   303  	if _, err := file.ReadAt(b, 0); err != nil {
   304  		return "", err
   305  	}
   306  	magic := string(b)
   307  	if magic != xfsMagic {
   308  		return "", fmt.Errorf("xfs magic not found")
   309  	}
   310  
   311  	// Filesystem UUID.
   312  	b = make([]byte, xfsUUIDSize)
   313  	if _, err := file.ReadAt(b, xfsUUIDOff); err != nil {
   314  		return "", err
   315  	}
   316  
   317  	return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]), nil
   318  }
   319  
   320  // GetGPTTable tries to read a GPT table from the block device described by the
   321  // passed BlockDev object, and returns a gpt.Table object, or an error if any
   322  func GetGPTTable(device BlockDev) (*gpt.Table, error) {
   323  	fd, err := os.Open(fmt.Sprintf("/dev/%s", device.Name))
   324  	if err != nil {
   325  		return nil, err
   326  	}
   327  	defer fd.Close()
   328  	if _, err = fd.Seek(512, io.SeekStart); err != nil {
   329  		return nil, err
   330  	}
   331  	table, err := gpt.ReadTable(fd, 512)
   332  	if err != nil {
   333  		return nil, err
   334  	}
   335  	return &table, nil
   336  }
   337  
   338  // FilterEFISystemPartitions returns a list of BlockDev objects whose underlying
   339  // block device is a valid EFI system partition, or an error if any
   340  func FilterEFISystemPartitions(devices []BlockDev) ([]BlockDev, error) {
   341  	return PartitionsByGUID(devices, SystemPartitionGUID.String())
   342  }
   343  
   344  // PartitionsByGUID returns a list of BlockDev objects whose underlying
   345  // block device has the given GUID
   346  func PartitionsByGUID(devices []BlockDev, guid string) ([]BlockDev, error) {
   347  	partitions := make([]BlockDev, 0)
   348  	for _, device := range devices {
   349  		table, err := GetGPTTable(device)
   350  		if err != nil {
   351  			log.Printf("Skipping %s: %v", device.Name, err)
   352  			continue
   353  		}
   354  		for _, part := range table.Partitions {
   355  			if part.IsEmpty() {
   356  				continue
   357  			}
   358  			if part.Type.String() == guid {
   359  				partitions = append(partitions, device)
   360  			}
   361  		}
   362  	}
   363  	return partitions, nil
   364  }
   365  
   366  // PartitionsByFsUUID returns a list of BlockDev objects whose underlying
   367  // block device has a filesystem with the given UUID
   368  func PartitionsByFsUUID(devices []BlockDev, fsuuid string) []BlockDev {
   369  	partitions := make([]BlockDev, 0)
   370  	for _, device := range devices {
   371  		if device.FsUUID == fsuuid {
   372  			partitions = append(partitions, device)
   373  		}
   374  	}
   375  	return partitions
   376  }
   377  
   378  // GetMountpointByDevice gets the mountpoint by given
   379  // device name. Returns on first match
   380  func GetMountpointByDevice(devicePath string) (*string, error) {
   381  	file, err := os.Open(LinuxMountsPath)
   382  	if err != nil {
   383  		return nil, err
   384  	}
   385  
   386  	defer file.Close()
   387  	scanner := bufio.NewScanner(file)
   388  
   389  	for scanner.Scan() {
   390  		deviceInfo := strings.Fields(scanner.Text())
   391  		if deviceInfo[0] == devicePath {
   392  			return &deviceInfo[1], nil
   393  		}
   394  	}
   395  
   396  	return nil, errors.New("Mountpoint not found")
   397  }