github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/gadget/ondisk.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019-2020 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package gadget
    21  
    22  import (
    23  	"encoding/json"
    24  	"fmt"
    25  	"os/exec"
    26  	"strconv"
    27  	"strings"
    28  
    29  	"github.com/snapcore/snapd/gadget/quantity"
    30  	"github.com/snapcore/snapd/osutil"
    31  )
    32  
    33  // sfdiskDeviceDump represents the sfdisk --dump JSON output format.
    34  type sfdiskDeviceDump struct {
    35  	PartitionTable sfdiskPartitionTable `json:"partitiontable"`
    36  }
    37  
    38  type sfdiskPartitionTable struct {
    39  	Label      string            `json:"label"`
    40  	ID         string            `json:"id"`
    41  	Device     string            `json:"device"`
    42  	Unit       string            `json:"unit"`
    43  	FirstLBA   uint64            `json:"firstlba"`
    44  	LastLBA    uint64            `json:"lastlba"`
    45  	Partitions []sfdiskPartition `json:"partitions"`
    46  }
    47  
    48  type sfdiskPartition struct {
    49  	Node  string `json:"node"`
    50  	Start uint64 `json:"start"`
    51  	Size  uint64 `json:"size"`
    52  	// List of GPT partition attributes in <attr>[ <attr>] format, numeric attributes
    53  	// are listed as GUID:<bit>[,<bit>]. Note that the even though the sfdisk(8) manpage
    54  	// says --part-attrs takes a space or comma separated list, the output from
    55  	// --json/--dump uses a different format.
    56  	Attrs string `json:"attrs"`
    57  	Type  string `json:"type"`
    58  	UUID  string `json:"uuid"`
    59  	Name  string `json:"name"`
    60  }
    61  
    62  // TODO: consider looking into merging LaidOutVolume/Structure OnDiskVolume/Structure
    63  
    64  // OnDiskStructure represents a gadget structure laid on a block device.
    65  type OnDiskStructure struct {
    66  	LaidOutStructure
    67  
    68  	// Node identifies the device node of the block device.
    69  	Node string
    70  
    71  	// Size of the on disk structure, which is at least equal to the
    72  	// LaidOutStructure.Size but may be bigger if the partition was
    73  	// expanded.
    74  	Size quantity.Size
    75  }
    76  
    77  // OnDiskVolume holds information about the disk device including its partitioning
    78  // schema, the partition table, and the structure layout it contains.
    79  type OnDiskVolume struct {
    80  	Structure []OnDiskStructure
    81  	ID        string
    82  	Device    string
    83  	Schema    string
    84  	// size in bytes
    85  	Size quantity.Size
    86  	// sector size in bytes
    87  	SectorSize quantity.Size
    88  }
    89  
    90  // OnDiskVolumeFromDevice obtains the partitioning and filesystem information from
    91  // the block device.
    92  func OnDiskVolumeFromDevice(device string) (*OnDiskVolume, error) {
    93  	output, err := exec.Command("sfdisk", "--json", device).Output()
    94  	if err != nil {
    95  		return nil, osutil.OutputErr(output, err)
    96  	}
    97  
    98  	var dump sfdiskDeviceDump
    99  	if err := json.Unmarshal(output, &dump); err != nil {
   100  		return nil, fmt.Errorf("cannot parse sfdisk output: %v", err)
   101  	}
   102  
   103  	dl, err := onDiskVolumeFromPartitionTable(dump.PartitionTable)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  	dl.Device = device
   108  
   109  	return dl, nil
   110  }
   111  
   112  func fromSfdiskPartitionType(st string, sfdiskLabel string) (string, error) {
   113  	switch sfdiskLabel {
   114  	case "dos":
   115  		// sometimes sfdisk reports what is "0C" in gadget.yaml as "c",
   116  		// normalize the values
   117  		v, err := strconv.ParseUint(st, 16, 8)
   118  		if err != nil {
   119  			return "", fmt.Errorf("cannot convert MBR partition type %q", st)
   120  		}
   121  		return fmt.Sprintf("%02X", v), nil
   122  	case "gpt":
   123  		return st, nil
   124  	default:
   125  		return "", fmt.Errorf("unsupported partitioning schema %q", sfdiskLabel)
   126  	}
   127  }
   128  
   129  func blockdevSizeCmd(cmd, devpath string) (quantity.Size, error) {
   130  	out, err := exec.Command("blockdev", cmd, devpath).CombinedOutput()
   131  	if err != nil {
   132  		return 0, osutil.OutputErr(out, err)
   133  	}
   134  	nospace := strings.TrimSpace(string(out))
   135  	sz, err := strconv.Atoi(nospace)
   136  	if err != nil {
   137  		return 0, fmt.Errorf("cannot parse blockdev %s result size %q: %v", cmd, nospace, err)
   138  	}
   139  	return quantity.Size(sz), nil
   140  }
   141  
   142  func blockDeviceSizeInSectors(devpath string) (quantity.Size, error) {
   143  	// the size is always reported in 512-byte sectors, even if the device does
   144  	// not have a physical sector size of 512
   145  	// XXX: consider using /sys/block/<dev>/size directly
   146  	return blockdevSizeCmd("--getsz", devpath)
   147  }
   148  
   149  func blockDeviceSectorSize(devpath string) (quantity.Size, error) {
   150  	// the size is reported in raw bytes
   151  	sz, err := blockdevSizeCmd("--getss", devpath)
   152  	if err != nil {
   153  		return 0, err
   154  	}
   155  
   156  	// ensure that the sector size is a multiple of 512, since we rely on that
   157  	// when we calculate the size in sectors, as blockdev --getsz always returns
   158  	// the size in 512-byte sectors
   159  	if sz%512 != 0 {
   160  		return 0, fmt.Errorf("cannot calculate structure size: sector size (%s) is not a multiple of 512", sz.String())
   161  	}
   162  	if sz == 0 {
   163  		// extra paranoia
   164  		return 0, fmt.Errorf("internal error: sector size returned as 0")
   165  	}
   166  	return sz, nil
   167  }
   168  
   169  // onDiskVolumeFromPartitionTable takes an sfdisk dump partition table and returns
   170  // the partitioning information as an on-disk volume.
   171  func onDiskVolumeFromPartitionTable(ptable sfdiskPartitionTable) (*OnDiskVolume, error) {
   172  	if ptable.Unit != "sectors" {
   173  		return nil, fmt.Errorf("cannot position partitions: unknown unit %q", ptable.Unit)
   174  	}
   175  
   176  	structure := make([]VolumeStructure, len(ptable.Partitions))
   177  	ds := make([]OnDiskStructure, len(ptable.Partitions))
   178  
   179  	sectorSize, err := blockDeviceSectorSize(ptable.Device)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  
   184  	for i, p := range ptable.Partitions {
   185  		info, err := filesystemInfo(p.Node)
   186  		if err != nil {
   187  			return nil, fmt.Errorf("cannot obtain filesystem information: %v", err)
   188  		}
   189  		switch {
   190  		case len(info.BlockDevices) == 0:
   191  			continue
   192  		case len(info.BlockDevices) > 1:
   193  			return nil, fmt.Errorf("unexpected number of blockdevices for node %q: %v", p.Node, info.BlockDevices)
   194  		}
   195  		bd := info.BlockDevices[0]
   196  
   197  		vsType, err := fromSfdiskPartitionType(p.Type, ptable.Label)
   198  		if err != nil {
   199  			return nil, fmt.Errorf("cannot convert sfdisk partition type %q: %v", p.Type, err)
   200  		}
   201  
   202  		structure[i] = VolumeStructure{
   203  			Name:       p.Name,
   204  			Size:       quantity.Size(p.Size) * sectorSize,
   205  			Label:      bd.Label,
   206  			Type:       vsType,
   207  			Filesystem: bd.FSType,
   208  		}
   209  
   210  		ds[i] = OnDiskStructure{
   211  			LaidOutStructure: LaidOutStructure{
   212  				VolumeStructure: &structure[i],
   213  				StartOffset:     quantity.Offset(p.Start) * quantity.Offset(sectorSize),
   214  				Index:           i + 1,
   215  			},
   216  			Node: p.Node,
   217  		}
   218  	}
   219  
   220  	var numSectors quantity.Size
   221  	if ptable.LastLBA != 0 {
   222  		// sfdisk reports the last usable LBA for GPT disks only
   223  		numSectors = quantity.Size(ptable.LastLBA + 1)
   224  	} else {
   225  		// sfdisk does not report any information about the size of a
   226  		// MBR partitioned disk, find out the size of the device by
   227  		// other means
   228  		sz, err := blockDeviceSizeInSectors(ptable.Device)
   229  		if err != nil {
   230  			return nil, fmt.Errorf("cannot obtain the size of device %q: %v", ptable.Device, err)
   231  		}
   232  
   233  		// since blockdev always reports the size in 512-byte sectors, if for
   234  		// some reason we are on a disk that does not 512-byte sectors, we will
   235  		// get confused, so in this case, multiply the number of 512-byte
   236  		// sectors by 512, then divide by the actual sector size to get the
   237  		// number of sectors
   238  
   239  		// this will never have a divisor, since we verified that sector size is
   240  		// a multiple of 512 above
   241  		numSectors = sz * 512 / sectorSize
   242  	}
   243  
   244  	dl := &OnDiskVolume{
   245  		Structure:  ds,
   246  		ID:         ptable.ID,
   247  		Device:     ptable.Device,
   248  		Schema:     ptable.Label,
   249  		Size:       numSectors * sectorSize,
   250  		SectorSize: sectorSize,
   251  	}
   252  
   253  	return dl, nil
   254  }
   255  
   256  // UpdatePartitionList re-reads the partitioning data from the device and
   257  // updates the volume structures in the specified volume.
   258  func UpdatePartitionList(dl *OnDiskVolume) error {
   259  	layout, err := OnDiskVolumeFromDevice(dl.Device)
   260  	if err != nil {
   261  		return fmt.Errorf("cannot read disk layout: %v", err)
   262  	}
   263  	if dl.ID != layout.ID {
   264  		return fmt.Errorf("partition table IDs don't match")
   265  	}
   266  
   267  	dl.Structure = layout.Structure
   268  	return nil
   269  }
   270  
   271  // lsblkFilesystemInfo represents the lsblk --fs JSON output format.
   272  type lsblkFilesystemInfo struct {
   273  	BlockDevices []lsblkBlockDevice `json:"blockdevices"`
   274  }
   275  
   276  type lsblkBlockDevice struct {
   277  	Name       string `json:"name"`
   278  	FSType     string `json:"fstype"`
   279  	Label      string `json:"label"`
   280  	UUID       string `json:"uuid"`
   281  	Mountpoint string `json:"mountpoint"`
   282  }
   283  
   284  func filesystemInfo(node string) (*lsblkFilesystemInfo, error) {
   285  	output, err := exec.Command("lsblk", "--fs", "--json", node).CombinedOutput()
   286  	if err != nil {
   287  		return nil, osutil.OutputErr(output, err)
   288  	}
   289  
   290  	var info lsblkFilesystemInfo
   291  	if err := json.Unmarshal(output, &info); err != nil {
   292  		return nil, fmt.Errorf("cannot parse lsblk output: %v", err)
   293  	}
   294  
   295  	return &info, nil
   296  }