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