github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/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  	"bytes"
    24  	"encoding/json"
    25  	"fmt"
    26  	"os/exec"
    27  	"strconv"
    28  	"strings"
    29  
    30  	"github.com/snapcore/snapd/logger"
    31  	"github.com/snapcore/snapd/osutil"
    32  	"github.com/snapcore/snapd/strutil"
    33  )
    34  
    35  const (
    36  	ubuntuBootLabel = "ubuntu-boot"
    37  	ubuntuSeedLabel = "ubuntu-seed"
    38  	ubuntuDataLabel = "ubuntu-data"
    39  
    40  	sectorSize Size = 512
    41  
    42  	createdPartitionAttr = "59"
    43  )
    44  
    45  var createdPartitionGUID = []string{
    46  	"0FC63DAF-8483-4772-8E79-3D69D8477DE4", // Linux filesystem data
    47  	"0657FD6D-A4AB-43C4-84E5-0933C84B4F4F", // Linux swap partition
    48  }
    49  
    50  // creationSupported returns whether we support and expect to create partitions
    51  // of the given type, it also means we are ready to remove them for re-installation
    52  // or retried installation if they are appropriately marked with createdPartitionAttr.
    53  func creationSupported(ptype string) bool {
    54  	return strutil.ListContains(createdPartitionGUID, strings.ToUpper(ptype))
    55  }
    56  
    57  // sfdiskDeviceDump represents the sfdisk --dump JSON output format.
    58  type sfdiskDeviceDump struct {
    59  	PartitionTable sfdiskPartitionTable `json:"partitiontable"`
    60  }
    61  
    62  type sfdiskPartitionTable struct {
    63  	Label      string            `json:"label"`
    64  	ID         string            `json:"id"`
    65  	Device     string            `json:"device"`
    66  	Unit       string            `json:"unit"`
    67  	FirstLBA   uint64            `json:"firstlba"`
    68  	LastLBA    uint64            `json:"lastlba"`
    69  	Partitions []sfdiskPartition `json:"partitions"`
    70  }
    71  
    72  type sfdiskPartition struct {
    73  	Node  string `json:"node"`
    74  	Start uint64 `json:"start"`
    75  	Size  uint64 `json:"size"`
    76  	// List of GPT partition attributes in <attr>[ <attr>] format, numeric attributes
    77  	// are listed as GUID:<bit>[,<bit>]. Note that the even though the sfdisk(8) manpage
    78  	// says --part-attrs takes a space or comma separated list, the output from
    79  	// --json/--dump uses a different format.
    80  	Attrs string `json:"attrs"`
    81  	Type  string `json:"type"`
    82  	UUID  string `json:"uuid"`
    83  	Name  string `json:"name"`
    84  }
    85  
    86  func isCreatedDuringInstall(p *sfdiskPartition, fs *lsblkBlockDevice, sfdiskLabel string) bool {
    87  	switch sfdiskLabel {
    88  	case "gpt":
    89  		// the created partitions use specific GPT GUID types and set a
    90  		// specific bit in partition attributes
    91  		if !creationSupported(p.Type) {
    92  			return false
    93  		}
    94  		for _, a := range strings.Fields(p.Attrs) {
    95  			if !strings.HasPrefix(a, "GUID:") {
    96  				continue
    97  			}
    98  			attrs := strings.Split(a[5:], ",")
    99  			if strutil.ListContains(attrs, createdPartitionAttr) {
   100  				return true
   101  			}
   102  		}
   103  	case "dos":
   104  		// we have no similar type/bit attribute setting for MBR, on top
   105  		// of that MBR does not support partition names, fall back to
   106  		// reasonable assumption that only partitions carrying
   107  		// ubuntu-boot and ubuntu-data labels are created during
   108  		// install, everything else was part of factory image
   109  
   110  		// TODO:UC20 consider using gadget layout information to build a
   111  		// mapping of partition start offset to label/name
   112  		createdDuringInstall := []string{ubuntuBootLabel, ubuntuDataLabel}
   113  		return strutil.ListContains(createdDuringInstall, fs.Label)
   114  	}
   115  	return false
   116  }
   117  
   118  // TODO: consider looking into merging LaidOutVolume/Structure OnDiskVolume/Structure
   119  
   120  // OnDiskStructure represents a gadget structure laid on a block device.
   121  type OnDiskStructure struct {
   122  	LaidOutStructure
   123  
   124  	// Node identifies the device node of the block device.
   125  	Node string
   126  	// CreatedDuringInstall is true when the structure has properties indicating
   127  	// it was created based on the gadget description during installation.
   128  	CreatedDuringInstall bool
   129  }
   130  
   131  // OnDiskVolume holds information about the disk device including its partitioning
   132  // schema, the partition table, and the structure layout it contains.
   133  type OnDiskVolume struct {
   134  	Structure []OnDiskStructure
   135  	ID        string
   136  	Device    string
   137  	Schema    string
   138  	// size in bytes
   139  	Size Size
   140  	// sector size in bytes
   141  	SectorSize     Size
   142  	partitionTable *sfdiskPartitionTable
   143  }
   144  
   145  // OnDiskVolumeFromDevice obtains the partitioning and filesystem information from
   146  // the block device.
   147  func OnDiskVolumeFromDevice(device string) (*OnDiskVolume, error) {
   148  	output, err := exec.Command("sfdisk", "--json", "-d", device).Output()
   149  	if err != nil {
   150  		return nil, osutil.OutputErr(output, err)
   151  	}
   152  
   153  	var dump sfdiskDeviceDump
   154  	if err := json.Unmarshal(output, &dump); err != nil {
   155  		return nil, fmt.Errorf("cannot parse sfdisk output: %v", err)
   156  	}
   157  
   158  	dl, err := onDiskVolumeFromPartitionTable(dump.PartitionTable)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  	dl.Device = device
   163  
   164  	return dl, nil
   165  }
   166  
   167  func fromSfdiskPartitionType(st string, sfdiskLabel string) (string, error) {
   168  	switch sfdiskLabel {
   169  	case "dos":
   170  		// sometimes sfdisk reports what is "0C" in gadget.yaml as "c",
   171  		// normalize the values
   172  		v, err := strconv.ParseUint(st, 16, 8)
   173  		if err != nil {
   174  			return "", fmt.Errorf("cannot convert MBR partition type %q", st)
   175  		}
   176  		return fmt.Sprintf("%02X", v), nil
   177  	case "gpt":
   178  		return st, nil
   179  	default:
   180  		return "", fmt.Errorf("unsupported partitioning schema %q", sfdiskLabel)
   181  	}
   182  }
   183  
   184  func blockDeviceSizeInSectors(devpath string) (Size, error) {
   185  	// the size is reported in 512-byte sectors
   186  	// XXX: consider using /sys/block/<dev>/size directly
   187  	out, err := exec.Command("blockdev", "--getsz", devpath).CombinedOutput()
   188  	if err != nil {
   189  		return 0, osutil.OutputErr(out, err)
   190  	}
   191  	nospace := strings.TrimSpace(string(out))
   192  	sz, err := strconv.Atoi(nospace)
   193  	if err != nil {
   194  		return 0, fmt.Errorf("cannot parse device size %q: %v", nospace, err)
   195  	}
   196  	return Size(sz), nil
   197  }
   198  
   199  // onDiskVolumeFromPartitionTable takes an sfdisk dump partition table and returns
   200  // the partitioning information as an on-disk volume.
   201  func onDiskVolumeFromPartitionTable(ptable sfdiskPartitionTable) (*OnDiskVolume, error) {
   202  	if ptable.Unit != "sectors" {
   203  		return nil, fmt.Errorf("cannot position partitions: unknown unit %q", ptable.Unit)
   204  	}
   205  
   206  	structure := make([]VolumeStructure, len(ptable.Partitions))
   207  	ds := make([]OnDiskStructure, len(ptable.Partitions))
   208  
   209  	for i, p := range ptable.Partitions {
   210  		info, err := filesystemInfo(p.Node)
   211  		if err != nil {
   212  			return nil, fmt.Errorf("cannot obtain filesystem information: %v", err)
   213  		}
   214  		switch {
   215  		case len(info.BlockDevices) == 0:
   216  			continue
   217  		case len(info.BlockDevices) > 1:
   218  			return nil, fmt.Errorf("unexpected number of blockdevices for node %q: %v", p.Node, info.BlockDevices)
   219  		}
   220  		bd := info.BlockDevices[0]
   221  
   222  		vsType, err := fromSfdiskPartitionType(p.Type, ptable.Label)
   223  		if err != nil {
   224  			return nil, fmt.Errorf("cannot convert sfdisk partition type %q: %v", p.Type, err)
   225  		}
   226  
   227  		structure[i] = VolumeStructure{
   228  			Name:       p.Name,
   229  			Size:       Size(p.Size) * sectorSize,
   230  			Label:      bd.Label,
   231  			Type:       vsType,
   232  			Filesystem: bd.FSType,
   233  		}
   234  
   235  		ds[i] = OnDiskStructure{
   236  			LaidOutStructure: LaidOutStructure{
   237  				VolumeStructure: &structure[i],
   238  				StartOffset:     Size(p.Start) * sectorSize,
   239  				Index:           i + 1,
   240  			},
   241  			Node:                 p.Node,
   242  			CreatedDuringInstall: isCreatedDuringInstall(&p, &bd, ptable.Label),
   243  		}
   244  	}
   245  
   246  	var numSectors Size
   247  	if ptable.LastLBA != 0 {
   248  		// sfdisk reports the last usable LBA for GPT disks only
   249  		numSectors = Size(ptable.LastLBA + 1)
   250  	} else {
   251  		// sfdisk does not report any information about the size of a
   252  		// MBR partitioned disk, find out the size of the device by
   253  		// other means
   254  		sz, err := blockDeviceSizeInSectors(ptable.Device)
   255  		if err != nil {
   256  			return nil, fmt.Errorf("cannot obtain the size of device %q: %v", ptable.Device, err)
   257  		}
   258  		numSectors = sz
   259  	}
   260  
   261  	dl := &OnDiskVolume{
   262  		Structure:      ds,
   263  		ID:             ptable.ID,
   264  		Device:         ptable.Device,
   265  		Schema:         ptable.Label,
   266  		Size:           numSectors * sectorSize,
   267  		SectorSize:     sectorSize,
   268  		partitionTable: &ptable,
   269  	}
   270  
   271  	return dl, nil
   272  }
   273  
   274  func deviceName(name string, index int) string {
   275  	if len(name) > 0 {
   276  		last := name[len(name)-1]
   277  		if last >= '0' && last <= '9' {
   278  			return fmt.Sprintf("%sp%d", name, index)
   279  		}
   280  	}
   281  	return fmt.Sprintf("%s%d", name, index)
   282  }
   283  
   284  // BuildPartitionList builds a list of partitions based on the current
   285  // device contents and gadget structure list, in sfdisk dump format, and
   286  // returns a partitioning description suitable for sfdisk input and a
   287  // list of the partitions to be created.
   288  func BuildPartitionList(dl *OnDiskVolume, pv *LaidOutVolume) (sfdiskInput *bytes.Buffer, toBeCreated []OnDiskStructure) {
   289  	ptable := dl.partitionTable
   290  
   291  	// Keep track what partitions we already have on disk
   292  	seen := map[uint64]bool{}
   293  	for _, p := range ptable.Partitions {
   294  		seen[p.Start] = true
   295  	}
   296  
   297  	// Check if the last partition has a system-data role
   298  	canExpandData := false
   299  	if n := len(pv.LaidOutStructure); n > 0 {
   300  		last := pv.LaidOutStructure[n-1]
   301  		if last.VolumeStructure.Role == SystemData {
   302  			canExpandData = true
   303  		}
   304  	}
   305  
   306  	// The partition index
   307  	pIndex := 0
   308  
   309  	// Write new partition data in named-fields format
   310  	buf := &bytes.Buffer{}
   311  	for _, p := range pv.LaidOutStructure {
   312  		if !p.IsPartition() {
   313  			continue
   314  		}
   315  
   316  		pIndex++
   317  		s := p.VolumeStructure
   318  
   319  		// Skip partitions that are already in the volume
   320  		start := p.StartOffset / sectorSize
   321  		if seen[uint64(start)] {
   322  			continue
   323  		}
   324  
   325  		// Only allow the creation of partitions with known GUIDs
   326  		// TODO:UC20: also provide a mechanism for MBR (RPi)
   327  		ptype := partitionType(ptable.Label, p.Type)
   328  		if ptable.Label == "gpt" && !creationSupported(ptype) {
   329  			logger.Noticef("cannot create partition with unsupported type %s", ptype)
   330  			continue
   331  		}
   332  
   333  		// Check if the data partition should be expanded
   334  		size := s.Size
   335  		if s.Role == SystemData && canExpandData && p.StartOffset+s.Size < dl.Size {
   336  			size = dl.Size - p.StartOffset
   337  		}
   338  
   339  		// Can we use the index here? Get the largest existing partition number and
   340  		// build from there could be safer if the disk partitions are not consecutive
   341  		// (can this actually happen in our images?)
   342  		node := deviceName(ptable.Device, pIndex)
   343  		fmt.Fprintf(buf, "%s : start=%12d, size=%12d, type=%s, name=%q, attrs=\"GUID:%s\"\n", node,
   344  			p.StartOffset/sectorSize, size/sectorSize, ptype, s.Name, createdPartitionAttr)
   345  
   346  		// Set expected labels based on role
   347  		switch s.Role {
   348  		case SystemBoot:
   349  			s.Label = ubuntuBootLabel
   350  		case SystemSeed:
   351  			s.Label = ubuntuSeedLabel
   352  		case SystemData:
   353  			s.Label = ubuntuDataLabel
   354  		}
   355  
   356  		toBeCreated = append(toBeCreated, OnDiskStructure{
   357  			LaidOutStructure:     p,
   358  			Node:                 node,
   359  			CreatedDuringInstall: true,
   360  		})
   361  	}
   362  
   363  	return buf, toBeCreated
   364  }
   365  
   366  // UpdatePartitionList re-reads the partitioning data from the device and
   367  // updates the partition list in the specified volume.
   368  func UpdatePartitionList(dl *OnDiskVolume) error {
   369  	layout, err := OnDiskVolumeFromDevice(dl.Device)
   370  	if err != nil {
   371  		return fmt.Errorf("cannot read disk layout: %v", err)
   372  	}
   373  	if dl.ID != layout.ID {
   374  		return fmt.Errorf("partition table IDs don't match")
   375  	}
   376  
   377  	dl.Structure = layout.Structure
   378  	dl.partitionTable = layout.partitionTable
   379  
   380  	return nil
   381  }
   382  
   383  // CreatedDuringInstall returns a list of partitions created during the
   384  // install process.
   385  func CreatedDuringInstall(layout *OnDiskVolume) (created []string) {
   386  	created = make([]string, 0, len(layout.Structure))
   387  	for _, s := range layout.Structure {
   388  		if s.CreatedDuringInstall {
   389  			created = append(created, s.Node)
   390  		}
   391  	}
   392  	return created
   393  }
   394  
   395  func partitionType(label, ptype string) string {
   396  	t := strings.Split(ptype, ",")
   397  	if len(t) < 1 {
   398  		return ""
   399  	}
   400  	if len(t) == 1 {
   401  		return t[0]
   402  	}
   403  	if label == "gpt" {
   404  		return t[1]
   405  	}
   406  	return t[0]
   407  }
   408  
   409  // lsblkFilesystemInfo represents the lsblk --fs JSON output format.
   410  type lsblkFilesystemInfo struct {
   411  	BlockDevices []lsblkBlockDevice `json:"blockdevices"`
   412  }
   413  
   414  type lsblkBlockDevice struct {
   415  	Name       string `json:"name"`
   416  	FSType     string `json:"fstype"`
   417  	Label      string `json:"label"`
   418  	UUID       string `json:"uuid"`
   419  	Mountpoint string `json:"mountpoint"`
   420  }
   421  
   422  func filesystemInfo(node string) (*lsblkFilesystemInfo, error) {
   423  	output, err := exec.Command("lsblk", "--fs", "--json", node).CombinedOutput()
   424  	if err != nil {
   425  		return nil, osutil.OutputErr(output, err)
   426  	}
   427  
   428  	var info lsblkFilesystemInfo
   429  	if err := json.Unmarshal(output, &info); err != nil {
   430  		return nil, fmt.Errorf("cannot parse lsblk output: %v", err)
   431  	}
   432  
   433  	return &info, nil
   434  }