github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/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  const (
    34  	sectorSize quantity.Size = 512
    35  )
    36  
    37  // sfdiskDeviceDump represents the sfdisk --dump JSON output format.
    38  type sfdiskDeviceDump struct {
    39  	PartitionTable sfdiskPartitionTable `json:"partitiontable"`
    40  }
    41  
    42  type sfdiskPartitionTable struct {
    43  	Label      string            `json:"label"`
    44  	ID         string            `json:"id"`
    45  	Device     string            `json:"device"`
    46  	Unit       string            `json:"unit"`
    47  	FirstLBA   uint64            `json:"firstlba"`
    48  	LastLBA    uint64            `json:"lastlba"`
    49  	Partitions []sfdiskPartition `json:"partitions"`
    50  }
    51  
    52  type sfdiskPartition struct {
    53  	Node  string `json:"node"`
    54  	Start uint64 `json:"start"`
    55  	Size  uint64 `json:"size"`
    56  	// List of GPT partition attributes in <attr>[ <attr>] format, numeric attributes
    57  	// are listed as GUID:<bit>[,<bit>]. Note that the even though the sfdisk(8) manpage
    58  	// says --part-attrs takes a space or comma separated list, the output from
    59  	// --json/--dump uses a different format.
    60  	Attrs string `json:"attrs"`
    61  	Type  string `json:"type"`
    62  	UUID  string `json:"uuid"`
    63  	Name  string `json:"name"`
    64  }
    65  
    66  // TODO: consider looking into merging LaidOutVolume/Structure OnDiskVolume/Structure
    67  
    68  // OnDiskStructure represents a gadget structure laid on a block device.
    69  type OnDiskStructure struct {
    70  	LaidOutStructure
    71  
    72  	// Node identifies the device node of the block device.
    73  	Node string
    74  
    75  	// Size of the on disk structure, which is at least equal to the
    76  	// LaidOutStructure.Size but may be bigger if the partition was
    77  	// expanded.
    78  	Size quantity.Size
    79  }
    80  
    81  // OnDiskVolume holds information about the disk device including its partitioning
    82  // schema, the partition table, and the structure layout it contains.
    83  type OnDiskVolume struct {
    84  	Structure []OnDiskStructure
    85  	ID        string
    86  	Device    string
    87  	Schema    string
    88  	// size in bytes
    89  	Size quantity.Size
    90  	// sector size in bytes
    91  	SectorSize quantity.Size
    92  }
    93  
    94  // OnDiskVolumeFromDevice obtains the partitioning and filesystem information from
    95  // the block device.
    96  func OnDiskVolumeFromDevice(device string) (*OnDiskVolume, error) {
    97  	output, err := exec.Command("sfdisk", "--json", device).Output()
    98  	if err != nil {
    99  		return nil, osutil.OutputErr(output, err)
   100  	}
   101  
   102  	var dump sfdiskDeviceDump
   103  	if err := json.Unmarshal(output, &dump); err != nil {
   104  		return nil, fmt.Errorf("cannot parse sfdisk output: %v", err)
   105  	}
   106  
   107  	dl, err := onDiskVolumeFromPartitionTable(dump.PartitionTable)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	dl.Device = device
   112  
   113  	return dl, nil
   114  }
   115  
   116  func fromSfdiskPartitionType(st string, sfdiskLabel string) (string, error) {
   117  	switch sfdiskLabel {
   118  	case "dos":
   119  		// sometimes sfdisk reports what is "0C" in gadget.yaml as "c",
   120  		// normalize the values
   121  		v, err := strconv.ParseUint(st, 16, 8)
   122  		if err != nil {
   123  			return "", fmt.Errorf("cannot convert MBR partition type %q", st)
   124  		}
   125  		return fmt.Sprintf("%02X", v), nil
   126  	case "gpt":
   127  		return st, nil
   128  	default:
   129  		return "", fmt.Errorf("unsupported partitioning schema %q", sfdiskLabel)
   130  	}
   131  }
   132  
   133  func blockDeviceSizeInSectors(devpath string) (quantity.Size, error) {
   134  	// the size is reported in 512-byte sectors
   135  	// XXX: consider using /sys/block/<dev>/size directly
   136  	out, err := exec.Command("blockdev", "--getsz", devpath).CombinedOutput()
   137  	if err != nil {
   138  		return 0, osutil.OutputErr(out, err)
   139  	}
   140  	nospace := strings.TrimSpace(string(out))
   141  	sz, err := strconv.Atoi(nospace)
   142  	if err != nil {
   143  		return 0, fmt.Errorf("cannot parse device size %q: %v", nospace, err)
   144  	}
   145  	return quantity.Size(sz), nil
   146  }
   147  
   148  // onDiskVolumeFromPartitionTable takes an sfdisk dump partition table and returns
   149  // the partitioning information as an on-disk volume.
   150  func onDiskVolumeFromPartitionTable(ptable sfdiskPartitionTable) (*OnDiskVolume, error) {
   151  	if ptable.Unit != "sectors" {
   152  		return nil, fmt.Errorf("cannot position partitions: unknown unit %q", ptable.Unit)
   153  	}
   154  
   155  	structure := make([]VolumeStructure, len(ptable.Partitions))
   156  	ds := make([]OnDiskStructure, len(ptable.Partitions))
   157  
   158  	for i, p := range ptable.Partitions {
   159  		info, err := filesystemInfo(p.Node)
   160  		if err != nil {
   161  			return nil, fmt.Errorf("cannot obtain filesystem information: %v", err)
   162  		}
   163  		switch {
   164  		case len(info.BlockDevices) == 0:
   165  			continue
   166  		case len(info.BlockDevices) > 1:
   167  			return nil, fmt.Errorf("unexpected number of blockdevices for node %q: %v", p.Node, info.BlockDevices)
   168  		}
   169  		bd := info.BlockDevices[0]
   170  
   171  		vsType, err := fromSfdiskPartitionType(p.Type, ptable.Label)
   172  		if err != nil {
   173  			return nil, fmt.Errorf("cannot convert sfdisk partition type %q: %v", p.Type, err)
   174  		}
   175  
   176  		structure[i] = VolumeStructure{
   177  			Name:       p.Name,
   178  			Size:       quantity.Size(p.Size) * sectorSize,
   179  			Label:      bd.Label,
   180  			Type:       vsType,
   181  			Filesystem: bd.FSType,
   182  		}
   183  
   184  		ds[i] = OnDiskStructure{
   185  			LaidOutStructure: LaidOutStructure{
   186  				VolumeStructure: &structure[i],
   187  				StartOffset:     quantity.Offset(p.Start) * quantity.Offset(sectorSize),
   188  				Index:           i + 1,
   189  			},
   190  			Node: p.Node,
   191  		}
   192  	}
   193  
   194  	var numSectors quantity.Size
   195  	if ptable.LastLBA != 0 {
   196  		// sfdisk reports the last usable LBA for GPT disks only
   197  		numSectors = quantity.Size(ptable.LastLBA + 1)
   198  	} else {
   199  		// sfdisk does not report any information about the size of a
   200  		// MBR partitioned disk, find out the size of the device by
   201  		// other means
   202  		sz, err := blockDeviceSizeInSectors(ptable.Device)
   203  		if err != nil {
   204  			return nil, fmt.Errorf("cannot obtain the size of device %q: %v", ptable.Device, err)
   205  		}
   206  		numSectors = sz
   207  	}
   208  
   209  	dl := &OnDiskVolume{
   210  		Structure:  ds,
   211  		ID:         ptable.ID,
   212  		Device:     ptable.Device,
   213  		Schema:     ptable.Label,
   214  		Size:       numSectors * sectorSize,
   215  		SectorSize: sectorSize,
   216  	}
   217  
   218  	return dl, nil
   219  }
   220  
   221  // UpdatePartitionList re-reads the partitioning data from the device and
   222  // updates the volume structures in the specified volume.
   223  func UpdatePartitionList(dl *OnDiskVolume) error {
   224  	layout, err := OnDiskVolumeFromDevice(dl.Device)
   225  	if err != nil {
   226  		return fmt.Errorf("cannot read disk layout: %v", err)
   227  	}
   228  	if dl.ID != layout.ID {
   229  		return fmt.Errorf("partition table IDs don't match")
   230  	}
   231  
   232  	dl.Structure = layout.Structure
   233  	return nil
   234  }
   235  
   236  // lsblkFilesystemInfo represents the lsblk --fs JSON output format.
   237  type lsblkFilesystemInfo struct {
   238  	BlockDevices []lsblkBlockDevice `json:"blockdevices"`
   239  }
   240  
   241  type lsblkBlockDevice struct {
   242  	Name       string `json:"name"`
   243  	FSType     string `json:"fstype"`
   244  	Label      string `json:"label"`
   245  	UUID       string `json:"uuid"`
   246  	Mountpoint string `json:"mountpoint"`
   247  }
   248  
   249  func filesystemInfo(node string) (*lsblkFilesystemInfo, error) {
   250  	output, err := exec.Command("lsblk", "--fs", "--json", node).CombinedOutput()
   251  	if err != nil {
   252  		return nil, osutil.OutputErr(output, err)
   253  	}
   254  
   255  	var info lsblkFilesystemInfo
   256  	if err := json.Unmarshal(output, &info); err != nil {
   257  		return nil, fmt.Errorf("cannot parse lsblk output: %v", err)
   258  	}
   259  
   260  	return &info, nil
   261  }