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