github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/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, 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.LaidOutVolumeFromGadget(gadgetRoot, model)
    67  	if err != nil {
    68  		return nil, fmt.Errorf("cannot layout the volume: %v", err)
    69  	}
    70  
    71  	// XXX: the only situation where auto-detect is not desired is
    72  	//      in (spread) testing - consider to remove forcing a device
    73  	//
    74  	// auto-detect device if no device is forced
    75  	if device == "" {
    76  		device, err = deviceFromRole(lv, gadget.SystemSeed)
    77  		if err != nil {
    78  			return nil, fmt.Errorf("cannot find device to create partitions on: %v", err)
    79  		}
    80  	}
    81  
    82  	diskLayout, err := gadget.OnDiskVolumeFromDevice(device)
    83  	if err != nil {
    84  		return nil, fmt.Errorf("cannot read %v partitions: %v", device, err)
    85  	}
    86  
    87  	// check if the current partition table is compatible with the gadget,
    88  	// ignoring partitions added by the installer (will be removed later)
    89  	if err := ensureLayoutCompatibility(lv, diskLayout); err != nil {
    90  		return nil, fmt.Errorf("gadget and %v partition table not compatible: %v", device, err)
    91  	}
    92  
    93  	// remove partitions added during a previous install attempt
    94  	if err := removeCreatedPartitions(lv, diskLayout); err != nil {
    95  		return nil, fmt.Errorf("cannot remove partitions from previous install: %v", err)
    96  	}
    97  	// at this point we removed any existing partition, nuke any
    98  	// of the existing sealed key files placed outside of the
    99  	// encrypted partitions (LP: #1879338)
   100  	sealedKeyFiles, _ := filepath.Glob(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "*.sealed-key"))
   101  	for _, keyFile := range sealedKeyFiles {
   102  		if err := os.Remove(keyFile); err != nil && !os.IsNotExist(err) {
   103  			return nil, fmt.Errorf("cannot cleanup obsolete key file: %v", keyFile)
   104  		}
   105  	}
   106  
   107  	created, err := createMissingPartitions(diskLayout, lv)
   108  	if err != nil {
   109  		return nil, fmt.Errorf("cannot create the partitions: %v", err)
   110  	}
   111  
   112  	makeKeySet := func() (*EncryptionKeySet, error) {
   113  		key, err := secboot.NewEncryptionKey()
   114  		if err != nil {
   115  			return nil, fmt.Errorf("cannot create encryption key: %v", err)
   116  		}
   117  
   118  		rkey, err := secboot.NewRecoveryKey()
   119  		if err != nil {
   120  			return nil, fmt.Errorf("cannot create recovery key: %v", err)
   121  		}
   122  		return &EncryptionKeySet{
   123  			Key:         key,
   124  			RecoveryKey: rkey,
   125  		}, nil
   126  	}
   127  	roleNeedsEncryption := func(role string) bool {
   128  		return role == gadget.SystemData || role == gadget.SystemSave
   129  	}
   130  	var keysForRoles map[string]*EncryptionKeySet
   131  
   132  	for _, part := range created {
   133  		roleFmt := ""
   134  		if part.Role != "" {
   135  			roleFmt = fmt.Sprintf("role %v", part.Role)
   136  		}
   137  		logger.Noticef("created new partition %v for structure %v (size %v) %s",
   138  			part.Node, part, part.Size.IECString(), roleFmt)
   139  		if options.Encrypt && roleNeedsEncryption(part.Role) {
   140  			keys, err := makeKeySet()
   141  			if err != nil {
   142  				return nil, err
   143  			}
   144  			logger.Noticef("encrypting partition device %v", part.Node)
   145  			dataPart, err := newEncryptedDevice(&part, keys.Key, part.Label)
   146  			if err != nil {
   147  				return nil, err
   148  			}
   149  
   150  			if err := dataPart.AddRecoveryKey(keys.Key, keys.RecoveryKey); err != nil {
   151  				return nil, err
   152  			}
   153  
   154  			// update the encrypted device node
   155  			part.Node = dataPart.Node
   156  			if keysForRoles == nil {
   157  				keysForRoles = map[string]*EncryptionKeySet{}
   158  			}
   159  			keysForRoles[part.Role] = keys
   160  			logger.Noticef("encrypted device %v", part.Node)
   161  		}
   162  
   163  		if err := makeFilesystem(&part); err != nil {
   164  			return nil, err
   165  		}
   166  
   167  		if err := writeContent(&part, gadgetRoot, observer); err != nil {
   168  			return nil, err
   169  		}
   170  
   171  		if options.Mount && part.Label != "" && part.HasFilesystem() {
   172  			if err := mountFilesystem(&part, boot.InitramfsRunMntDir); err != nil {
   173  				return nil, err
   174  			}
   175  		}
   176  	}
   177  
   178  	return &InstalledSystemSideData{
   179  		KeysForRoles: keysForRoles,
   180  	}, nil
   181  }
   182  
   183  // isCreatableAtInstall returns whether the gadget structure would be created at
   184  // install - currently that is only ubuntu-save, ubuntu-data, and ubuntu-boot
   185  func isCreatableAtInstall(gv *gadget.VolumeStructure) bool {
   186  	// a structure is creatable at install if it is one of the roles for
   187  	// system-save, system-data, or system-boot
   188  	switch gv.Role {
   189  	case gadget.SystemSave, gadget.SystemData, gadget.SystemBoot:
   190  		return true
   191  	default:
   192  		return false
   193  	}
   194  }
   195  
   196  func ensureLayoutCompatibility(gadgetLayout *gadget.LaidOutVolume, diskLayout *gadget.OnDiskVolume) error {
   197  	eq := func(ds gadget.OnDiskStructure, gs gadget.LaidOutStructure) (bool, string) {
   198  		dv := ds.VolumeStructure
   199  		gv := gs.VolumeStructure
   200  		nameMatch := gv.Name == dv.Name
   201  		if gadgetLayout.Schema == "mbr" {
   202  			// partitions have no names in MBR so bypass the name check
   203  			nameMatch = true
   204  		}
   205  		// Previous installation may have failed before filesystem creation or
   206  		// partition may be encrypted, so if the on disk offset matches the
   207  		// gadget offset, and the gadget structure is creatable during install,
   208  		// then they are equal
   209  		// otherwise, if they are not created during installation, the
   210  		// filesystem must be the same
   211  		check := nameMatch && ds.StartOffset == gs.StartOffset && (isCreatableAtInstall(gv) || dv.Filesystem == gv.Filesystem)
   212  		sizeMatches := dv.Size == gv.Size
   213  		if gv.Role == gadget.SystemData {
   214  			// system-data may have been expanded
   215  			sizeMatches = dv.Size >= gv.Size
   216  		}
   217  		if check && sizeMatches {
   218  			return true, ""
   219  		}
   220  		switch {
   221  		case !nameMatch:
   222  			// don't return a reason if the names don't match
   223  			return false, ""
   224  		case ds.StartOffset != gs.StartOffset:
   225  			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())
   226  		case !isCreatableAtInstall(gv) && dv.Filesystem != gv.Filesystem:
   227  			return false, "filesystems do not match and the partition is not creatable at install"
   228  		case dv.Size < gv.Size:
   229  			return false, "on disk size is smaller than gadget size"
   230  		case gv.Role != gadget.SystemData && dv.Size > gv.Size:
   231  			return false, "on disk size is larger than gadget size (and the role should not be expanded)"
   232  		default:
   233  			return false, "some other logic condition (should be impossible?)"
   234  		}
   235  	}
   236  
   237  	contains := func(haystack []gadget.LaidOutStructure, needle gadget.OnDiskStructure) (bool, string) {
   238  		reasonAbsent := ""
   239  		for _, h := range haystack {
   240  			matches, reasonNotMatches := eq(needle, h)
   241  			if matches {
   242  				return true, ""
   243  			}
   244  			// this has the effect of only returning the last non-empty reason
   245  			// string
   246  			if reasonNotMatches != "" {
   247  				reasonAbsent = reasonNotMatches
   248  			}
   249  		}
   250  		return false, reasonAbsent
   251  	}
   252  
   253  	if gadgetLayout.Size > diskLayout.Size {
   254  		return fmt.Errorf("device %v (%s) is too small to fit the requested layout (%s)", diskLayout.Device,
   255  			diskLayout.Size.IECString(), gadgetLayout.Size.IECString())
   256  	}
   257  
   258  	// Check if top level properties match
   259  	if !isCompatibleSchema(gadgetLayout.Volume.Schema, diskLayout.Schema) {
   260  		return fmt.Errorf("disk partitioning schema %q doesn't match gadget schema %q", diskLayout.Schema, gadgetLayout.Volume.Schema)
   261  	}
   262  	if gadgetLayout.Volume.ID != "" && gadgetLayout.Volume.ID != diskLayout.ID {
   263  		return fmt.Errorf("disk ID %q doesn't match gadget volume ID %q", diskLayout.ID, gadgetLayout.Volume.ID)
   264  	}
   265  
   266  	// Check if all existing device partitions are also in gadget
   267  	for _, ds := range diskLayout.Structure {
   268  		present, reasonAbsent := contains(gadgetLayout.LaidOutStructure, ds)
   269  		if !present {
   270  			if reasonAbsent != "" {
   271  				// use the right format so that it can be
   272  				// appended to the error message
   273  				reasonAbsent = fmt.Sprintf(": %s", reasonAbsent)
   274  			}
   275  			return fmt.Errorf("cannot find disk partition %s (starting at %d) in gadget%s", ds.Node, ds.StartOffset, reasonAbsent)
   276  		}
   277  	}
   278  
   279  	return nil
   280  }
   281  
   282  func isCompatibleSchema(gadgetSchema, diskSchema string) bool {
   283  	switch gadgetSchema {
   284  	// XXX: "mbr,gpt" is currently unsupported
   285  	case "", "gpt":
   286  		return diskSchema == "gpt"
   287  	case "mbr":
   288  		return diskSchema == "dos"
   289  	default:
   290  		return false
   291  	}
   292  }