github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/gadget/install/install.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  // +build !nosecboot
     3  
     4  /*
     5   * Copyright (C) 2019-2020 Canonical Ltd
     6   *
     7   * This program is free software: you can redistribute it and/or modify
     8   * it under the terms of the GNU General Public License version 3 as
     9   * published by the Free Software Foundation.
    10   *
    11   * This program is distributed in the hope that it will be useful,
    12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14   * GNU General Public License for more details.
    15   *
    16   * You should have received a copy of the GNU General Public License
    17   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18   *
    19   */
    20  
    21  package install
    22  
    23  import (
    24  	"fmt"
    25  	"os"
    26  	"path/filepath"
    27  
    28  	"github.com/snapcore/snapd/boot"
    29  	"github.com/snapcore/snapd/gadget"
    30  	"github.com/snapcore/snapd/logger"
    31  	"github.com/snapcore/snapd/secboot"
    32  )
    33  
    34  const (
    35  	ubuntuDataLabel = "ubuntu-data"
    36  	ubuntuSaveLabel = "ubuntu-save"
    37  )
    38  
    39  func deviceFromRole(lv *gadget.LaidOutVolume, role string) (device string, err error) {
    40  	for _, vs := range lv.LaidOutStructure {
    41  		// XXX: this part of the finding maybe should be a
    42  		// method on gadget.*Volume
    43  		if vs.Role == role {
    44  			device, err = gadget.FindDeviceForStructure(&vs)
    45  			if err != nil {
    46  				return "", fmt.Errorf("cannot find device for role %q: %v", role, err)
    47  			}
    48  			return gadget.ParentDiskFromMountSource(device)
    49  		}
    50  	}
    51  	return "", fmt.Errorf("cannot find role %s in gadget", role)
    52  }
    53  
    54  // Run bootstraps the partitions of a device, by either creating
    55  // missing ones or recreating installed ones.
    56  func Run(model gadget.Model, gadgetRoot, kernelRoot, device string, options Options, observer gadget.ContentObserver) (*InstalledSystemSideData, error) {
    57  	logger.Noticef("installing a new system")
    58  	logger.Noticef("        gadget data from: %v", gadgetRoot)
    59  	if options.Encrypt {
    60  		logger.Noticef("        encryption: on")
    61  	}
    62  	if gadgetRoot == "" {
    63  		return nil, fmt.Errorf("cannot use empty gadget root directory")
    64  	}
    65  
    66  	lv, err := gadget.LaidOutSystemVolumeFromGadget(gadgetRoot, kernelRoot, model)
    67  	if err != nil {
    68  		return nil, fmt.Errorf("cannot layout the volume: %v", err)
    69  	}
    70  	// TODO: resolve content paths from gadget here
    71  
    72  	// XXX: the only situation where auto-detect is not desired is
    73  	//      in (spread) testing - consider to remove forcing a device
    74  	//
    75  	// auto-detect device if no device is forced
    76  	if device == "" {
    77  		device, err = deviceFromRole(lv, gadget.SystemSeed)
    78  		if err != nil {
    79  			return nil, fmt.Errorf("cannot find device to create partitions on: %v", err)
    80  		}
    81  	}
    82  
    83  	diskLayout, err := gadget.OnDiskVolumeFromDevice(device)
    84  	if err != nil {
    85  		return nil, fmt.Errorf("cannot read %v partitions: %v", device, err)
    86  	}
    87  
    88  	// check if the current partition table is compatible with the gadget,
    89  	// ignoring partitions added by the installer (will be removed later)
    90  	if err := ensureLayoutCompatibility(lv, diskLayout); err != nil {
    91  		return nil, fmt.Errorf("gadget and %v partition table not compatible: %v", device, err)
    92  	}
    93  
    94  	// remove partitions added during a previous install attempt
    95  	if err := removeCreatedPartitions(lv, diskLayout); err != nil {
    96  		return nil, fmt.Errorf("cannot remove partitions from previous install: %v", err)
    97  	}
    98  	// at this point we removed any existing partition, nuke any
    99  	// of the existing sealed key files placed outside of the
   100  	// encrypted partitions (LP: #1879338)
   101  	sealedKeyFiles, _ := filepath.Glob(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "*.sealed-key"))
   102  	for _, keyFile := range sealedKeyFiles {
   103  		if err := os.Remove(keyFile); err != nil && !os.IsNotExist(err) {
   104  			return nil, fmt.Errorf("cannot cleanup obsolete key file: %v", keyFile)
   105  		}
   106  	}
   107  
   108  	created, err := createMissingPartitions(diskLayout, lv)
   109  	if err != nil {
   110  		return nil, fmt.Errorf("cannot create the partitions: %v", err)
   111  	}
   112  
   113  	makeKeySet := func() (*EncryptionKeySet, error) {
   114  		key, err := secboot.NewEncryptionKey()
   115  		if err != nil {
   116  			return nil, fmt.Errorf("cannot create encryption key: %v", err)
   117  		}
   118  
   119  		rkey, err := secboot.NewRecoveryKey()
   120  		if err != nil {
   121  			return nil, fmt.Errorf("cannot create recovery key: %v", err)
   122  		}
   123  		return &EncryptionKeySet{
   124  			Key:         key,
   125  			RecoveryKey: rkey,
   126  		}, nil
   127  	}
   128  	roleNeedsEncryption := func(role string) bool {
   129  		return role == gadget.SystemData || role == gadget.SystemSave
   130  	}
   131  	var keysForRoles map[string]*EncryptionKeySet
   132  
   133  	for _, part := range created {
   134  		roleFmt := ""
   135  		if part.Role != "" {
   136  			roleFmt = fmt.Sprintf("role %v", part.Role)
   137  		}
   138  		logger.Noticef("created new partition %v for structure %v (size %v) %s",
   139  			part.Node, part, part.Size.IECString(), roleFmt)
   140  		if options.Encrypt && roleNeedsEncryption(part.Role) {
   141  			keys, err := makeKeySet()
   142  			if err != nil {
   143  				return nil, err
   144  			}
   145  			logger.Noticef("encrypting partition device %v", part.Node)
   146  			dataPart, err := newEncryptedDevice(&part, keys.Key, part.Label)
   147  			if err != nil {
   148  				return nil, err
   149  			}
   150  
   151  			if err := dataPart.AddRecoveryKey(keys.Key, keys.RecoveryKey); err != nil {
   152  				return nil, err
   153  			}
   154  
   155  			// update the encrypted device node
   156  			part.Node = dataPart.Node
   157  			if keysForRoles == nil {
   158  				keysForRoles = map[string]*EncryptionKeySet{}
   159  			}
   160  			keysForRoles[part.Role] = keys
   161  			logger.Noticef("encrypted device %v", part.Node)
   162  		}
   163  
   164  		// use the diskLayout.SectorSize here instead of lv.SectorSize, we check
   165  		// that if there is a sector-size specified in the gadget that it
   166  		// matches what is on the disk, but sometimes there may not be a sector
   167  		// size specified in the gadget.yaml, but we will always have the sector
   168  		// size from the physical disk device
   169  		if err := makeFilesystem(&part, diskLayout.SectorSize); err != nil {
   170  			return nil, fmt.Errorf("cannot make filesystem for partition %s: %v", part.Role, err)
   171  		}
   172  
   173  		if err := writeContent(&part, gadgetRoot, observer); err != nil {
   174  			return nil, err
   175  		}
   176  
   177  		if options.Mount && part.Label != "" && part.HasFilesystem() {
   178  			if err := mountFilesystem(&part, boot.InitramfsRunMntDir); err != nil {
   179  				return nil, err
   180  			}
   181  		}
   182  	}
   183  
   184  	return &InstalledSystemSideData{
   185  		KeysForRoles: keysForRoles,
   186  	}, nil
   187  }
   188  
   189  // isCreatableAtInstall returns whether the gadget structure would be created at
   190  // install - currently that is only ubuntu-save, ubuntu-data, and ubuntu-boot
   191  func isCreatableAtInstall(gv *gadget.VolumeStructure) bool {
   192  	// a structure is creatable at install if it is one of the roles for
   193  	// system-save, system-data, or system-boot
   194  	switch gv.Role {
   195  	case gadget.SystemSave, gadget.SystemData, gadget.SystemBoot:
   196  		return true
   197  	default:
   198  		return false
   199  	}
   200  }
   201  
   202  func ensureLayoutCompatibility(gadgetLayout *gadget.LaidOutVolume, diskLayout *gadget.OnDiskVolume) error {
   203  	eq := func(ds gadget.OnDiskStructure, gs gadget.LaidOutStructure) (bool, string) {
   204  		dv := ds.VolumeStructure
   205  		gv := gs.VolumeStructure
   206  		nameMatch := gv.Name == dv.Name
   207  		if gadgetLayout.Schema == "mbr" {
   208  			// partitions have no names in MBR so bypass the name check
   209  			nameMatch = true
   210  		}
   211  		// Previous installation may have failed before filesystem creation or
   212  		// partition may be encrypted, so if the on disk offset matches the
   213  		// gadget offset, and the gadget structure is creatable during install,
   214  		// then they are equal
   215  		// otherwise, if they are not created during installation, the
   216  		// filesystem must be the same
   217  		check := nameMatch && ds.StartOffset == gs.StartOffset && (isCreatableAtInstall(gv) || dv.Filesystem == gv.Filesystem)
   218  		sizeMatches := dv.Size == gv.Size
   219  		if gv.Role == gadget.SystemData {
   220  			// system-data may have been expanded
   221  			sizeMatches = dv.Size >= gv.Size
   222  		}
   223  		if check && sizeMatches {
   224  			return true, ""
   225  		}
   226  		switch {
   227  		case !nameMatch:
   228  			// don't return a reason if the names don't match
   229  			return false, ""
   230  		case ds.StartOffset != gs.StartOffset:
   231  			return false, fmt.Sprintf("start offsets do not match (disk: %d (%s) and gadget: %d (%s))", ds.StartOffset, ds.StartOffset.IECString(), gs.StartOffset, gs.StartOffset.IECString())
   232  		case !isCreatableAtInstall(gv) && dv.Filesystem != gv.Filesystem:
   233  			return false, "filesystems do not match and the partition is not creatable at install"
   234  		case dv.Size < gv.Size:
   235  			return false, "on disk size is smaller than gadget size"
   236  		case gv.Role != gadget.SystemData && dv.Size > gv.Size:
   237  			return false, "on disk size is larger than gadget size (and the role should not be expanded)"
   238  		default:
   239  			return false, "some other logic condition (should be impossible?)"
   240  		}
   241  	}
   242  
   243  	contains := func(haystack []gadget.LaidOutStructure, needle gadget.OnDiskStructure) (bool, string) {
   244  		reasonAbsent := ""
   245  		for _, h := range haystack {
   246  			matches, reasonNotMatches := eq(needle, h)
   247  			if matches {
   248  				return true, ""
   249  			}
   250  			// this has the effect of only returning the last non-empty reason
   251  			// string
   252  			if reasonNotMatches != "" {
   253  				reasonAbsent = reasonNotMatches
   254  			}
   255  		}
   256  		return false, reasonAbsent
   257  	}
   258  
   259  	// check size of volumes
   260  	if gadgetLayout.Size > diskLayout.Size {
   261  		return fmt.Errorf("device %v (%s) is too small to fit the requested layout (%s)", diskLayout.Device,
   262  			diskLayout.Size.IECString(), gadgetLayout.Size.IECString())
   263  	}
   264  
   265  	// check that the sizes of all structures in the gadget are multiples of
   266  	// the disk sector size (unless the structure is the MBR)
   267  	for _, ls := range gadgetLayout.LaidOutStructure {
   268  		if !gadget.IsRoleMBR(ls) {
   269  			if ls.Size%diskLayout.SectorSize != 0 {
   270  				return fmt.Errorf("gadget volume structure %v size is not a multiple of disk sector size %v",
   271  					ls, diskLayout.SectorSize)
   272  			}
   273  		}
   274  	}
   275  
   276  	// Check if top level properties match
   277  	if !isCompatibleSchema(gadgetLayout.Volume.Schema, diskLayout.Schema) {
   278  		return fmt.Errorf("disk partitioning schema %q doesn't match gadget schema %q", diskLayout.Schema, gadgetLayout.Volume.Schema)
   279  	}
   280  	if gadgetLayout.Volume.ID != "" && gadgetLayout.Volume.ID != diskLayout.ID {
   281  		return fmt.Errorf("disk ID %q doesn't match gadget volume ID %q", diskLayout.ID, gadgetLayout.Volume.ID)
   282  	}
   283  
   284  	// Check if all existing device partitions are also in gadget
   285  	for _, ds := range diskLayout.Structure {
   286  		present, reasonAbsent := contains(gadgetLayout.LaidOutStructure, ds)
   287  		if !present {
   288  			if reasonAbsent != "" {
   289  				// use the right format so that it can be
   290  				// appended to the error message
   291  				reasonAbsent = fmt.Sprintf(": %s", reasonAbsent)
   292  			}
   293  			return fmt.Errorf("cannot find disk partition %s (starting at %d) in gadget%s", ds.Node, ds.StartOffset, reasonAbsent)
   294  		}
   295  	}
   296  
   297  	return nil
   298  }
   299  
   300  func isCompatibleSchema(gadgetSchema, diskSchema string) bool {
   301  	switch gadgetSchema {
   302  	// XXX: "mbr,gpt" is currently unsupported
   303  	case "", "gpt":
   304  		return diskSchema == "gpt"
   305  	case "mbr":
   306  		return diskSchema == "dos"
   307  	default:
   308  		return false
   309  	}
   310  }