github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/devicestate/handlers_install.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2021 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 devicestate
    21  
    22  import (
    23  	"compress/gzip"
    24  	"fmt"
    25  	"os"
    26  	"os/exec"
    27  	"path/filepath"
    28  
    29  	"gopkg.in/tomb.v2"
    30  
    31  	"github.com/snapcore/snapd/asserts"
    32  	"github.com/snapcore/snapd/boot"
    33  	"github.com/snapcore/snapd/gadget"
    34  	"github.com/snapcore/snapd/gadget/install"
    35  	"github.com/snapcore/snapd/logger"
    36  	"github.com/snapcore/snapd/osutil"
    37  	"github.com/snapcore/snapd/overlord/snapstate"
    38  	"github.com/snapcore/snapd/overlord/state"
    39  	"github.com/snapcore/snapd/randutil"
    40  	"github.com/snapcore/snapd/secboot"
    41  	"github.com/snapcore/snapd/sysconfig"
    42  	"github.com/snapcore/snapd/timings"
    43  )
    44  
    45  var (
    46  	bootMakeRunnable            = boot.MakeRunnableSystem
    47  	bootEnsureNextBootToRunMode = boot.EnsureNextBootToRunMode
    48  	installRun                  = install.Run
    49  
    50  	sysconfigConfigureTargetSystem = sysconfig.ConfigureTargetSystem
    51  )
    52  
    53  func setSysconfigCloudOptions(opts *sysconfig.Options, gadgetDir string, model *asserts.Model) {
    54  	ubuntuSeedCloudCfg := filepath.Join(boot.InitramfsUbuntuSeedDir, "data/etc/cloud/cloud.cfg.d")
    55  
    56  	// TODO:UC20: on grade signed, allow files from ubuntu-seed, but do
    57  	//            filtering on the resultant cloud config
    58  	shouldUseUbuntuSeed := model.Grade() == asserts.ModelDangerous && osutil.IsDirectory(ubuntuSeedCloudCfg)
    59  
    60  	switch {
    61  	// if the gadget has a cloud.conf file, always use that regardless of grade
    62  	case sysconfig.HasGadgetCloudConf(gadgetDir):
    63  		// this is implicitly handled by ConfigureTargetSystem when it
    64  		// configures cloud-init, so we just need to allow cloud-init for the
    65  		// gadget config to be used, but we also should check to see if
    66  		// ubuntu-seed config should be allowed as well
    67  		opts.AllowCloudInit = true
    68  		if shouldUseUbuntuSeed {
    69  			opts.CloudInitSrcDir = ubuntuSeedCloudCfg
    70  		}
    71  
    72  	// next thing is if are in secured grade and didn't have gadget config, we
    73  	// disable cloud-init always, clouds should have their own config via
    74  	// gadgets for grade secured
    75  	case model.Grade() == asserts.ModelSecured:
    76  		opts.AllowCloudInit = false
    77  
    78  	// next if we are grade dangerous, then we also install cloud configuration
    79  	// from ubuntu-seed if it exists
    80  	case shouldUseUbuntuSeed:
    81  		opts.AllowCloudInit = true
    82  		opts.CloudInitSrcDir = ubuntuSeedCloudCfg
    83  
    84  	// note that if none of the conditions were true, it means we are on grade
    85  	// dangerous or signed, and cloud-init is still allowed to run without
    86  	// additional configuration on first-boot, so that NoCloud CIDATA can be
    87  	// provided for example
    88  	default:
    89  		opts.AllowCloudInit = true
    90  	}
    91  }
    92  
    93  func writeModel(model *asserts.Model, where string) error {
    94  	f, err := os.OpenFile(where, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
    95  	if err != nil {
    96  		return err
    97  	}
    98  	defer f.Close()
    99  	return asserts.NewEncoder(f).Encode(model)
   100  }
   101  
   102  func writeLogs(rootdir string) error {
   103  	// XXX: would be great to use native journal format but it's tied
   104  	//      to machine-id, we could journal -o export but there
   105  	//      is no systemd-journal-remote on core{,18,20}
   106  	//
   107  	// XXX: or only log if persistent journal is enabled?
   108  	logPath := filepath.Join(rootdir, "var/log/install-mode.log.gz")
   109  	if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil {
   110  		return err
   111  	}
   112  
   113  	f, err := os.Create(logPath)
   114  	if err != nil {
   115  		return err
   116  	}
   117  	defer f.Close()
   118  
   119  	gz := gzip.NewWriter(f)
   120  	defer gz.Close()
   121  
   122  	cmd := exec.Command("journalctl", "-b", "0")
   123  	cmd.Stdout = gz
   124  	if err := cmd.Run(); err != nil {
   125  		return fmt.Errorf("cannot collect journal output: %v", err)
   126  	}
   127  	if err := gz.Flush(); err != nil {
   128  		return fmt.Errorf("cannot flush compressed log output: %v", err)
   129  	}
   130  
   131  	return nil
   132  }
   133  
   134  func writeTimings(st *state.State, rootdir string) error {
   135  	logPath := filepath.Join(rootdir, "var/log/install-timings.txt.gz")
   136  	if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil {
   137  		return err
   138  	}
   139  
   140  	f, err := os.Create(logPath)
   141  	if err != nil {
   142  		return err
   143  	}
   144  	defer f.Close()
   145  
   146  	gz := gzip.NewWriter(f)
   147  	defer gz.Close()
   148  
   149  	var chgIDs []string
   150  	for _, chg := range st.Changes() {
   151  		if chg.Kind() == "seed" {
   152  			// this is captured via "--ensure=seed" below
   153  			continue
   154  		}
   155  		chgIDs = append(chgIDs, chg.ID())
   156  	}
   157  
   158  	// state must be unlocked for "snap changes/debug timings" to work
   159  	st.Unlock()
   160  	defer st.Lock()
   161  
   162  	// XXX: ugly, ugly, but using the internal timings requires
   163  	//      some refactor as a lot of the required bits are not
   164  	//      exported right now
   165  	// first all changes
   166  	fmt.Fprintf(gz, "---- Output of: snap changes\n")
   167  	cmd := exec.Command("snap", "changes")
   168  	cmd.Stdout = gz
   169  	if err := cmd.Run(); err != nil {
   170  		return fmt.Errorf("cannot collect timings output: %v", err)
   171  	}
   172  	fmt.Fprintf(gz, "\n")
   173  	// then the seeding
   174  	fmt.Fprintf(gz, "---- Output of snap debug timings --ensure=seed\n")
   175  	cmd = exec.Command("snap", "debug", "timings", "--ensure=seed")
   176  	cmd.Stdout = gz
   177  	if err := cmd.Run(); err != nil {
   178  		return fmt.Errorf("cannot collect timings output: %v", err)
   179  	}
   180  	fmt.Fprintf(gz, "\n")
   181  	// then the changes
   182  	for _, chgID := range chgIDs {
   183  		fmt.Fprintf(gz, "---- Output of snap debug timings %s\n", chgID)
   184  		cmd = exec.Command("snap", "debug", "timings", chgID)
   185  		cmd.Stdout = gz
   186  		if err := cmd.Run(); err != nil {
   187  			return fmt.Errorf("cannot collect timings output: %v", err)
   188  		}
   189  		fmt.Fprintf(gz, "\n")
   190  	}
   191  
   192  	if err := gz.Flush(); err != nil {
   193  		return fmt.Errorf("cannot flush timings output: %v", err)
   194  	}
   195  
   196  	return nil
   197  }
   198  
   199  func (m *DeviceManager) doSetupRunSystem(t *state.Task, _ *tomb.Tomb) error {
   200  	st := t.State()
   201  	st.Lock()
   202  	defer st.Unlock()
   203  
   204  	perfTimings := state.TimingsForTask(t)
   205  	defer perfTimings.Save(st)
   206  
   207  	// get gadget dir
   208  	deviceCtx, err := DeviceCtx(st, t, nil)
   209  	if err != nil {
   210  		return fmt.Errorf("cannot get device context: %v", err)
   211  	}
   212  	gadgetInfo, err := snapstate.GadgetInfo(st, deviceCtx)
   213  	if err != nil {
   214  		return fmt.Errorf("cannot get gadget info: %v", err)
   215  	}
   216  	gadgetDir := gadgetInfo.MountDir()
   217  
   218  	kernelInfo, err := snapstate.KernelInfo(st, deviceCtx)
   219  	if err != nil {
   220  		return fmt.Errorf("cannot get kernel info: %v", err)
   221  	}
   222  	kernelDir := kernelInfo.MountDir()
   223  
   224  	modeEnv, err := maybeReadModeenv()
   225  	if err != nil {
   226  		return err
   227  	}
   228  	if modeEnv == nil {
   229  		return fmt.Errorf("missing modeenv, cannot proceed")
   230  	}
   231  
   232  	// bootstrap
   233  	bopts := install.Options{
   234  		Mount: true,
   235  	}
   236  	useEncryption, err := m.checkEncryption(st, deviceCtx)
   237  	if err != nil {
   238  		return err
   239  	}
   240  	bopts.Encrypt = useEncryption
   241  
   242  	model := deviceCtx.Model()
   243  
   244  	// make sure that gadget is usable for the set up we want to use it in
   245  	validationConstraints := gadget.ValidationConstraints{
   246  		EncryptedData: useEncryption,
   247  	}
   248  	var ginfo *gadget.Info
   249  	timings.Run(perfTimings, "read-info-and-validate", "Read and validate gagdet info", func(timings.Measurer) {
   250  		ginfo, err = gadget.ReadInfoAndValidate(gadgetDir, model, &validationConstraints)
   251  	})
   252  	if err != nil {
   253  		return fmt.Errorf("cannot use gadget: %v", err)
   254  	}
   255  	if err := gadget.ValidateContent(ginfo, gadgetDir, kernelDir); err != nil {
   256  		return fmt.Errorf("cannot use gadget: %v", err)
   257  	}
   258  
   259  	var trustedInstallObserver *boot.TrustedAssetsInstallObserver
   260  	// get a nice nil interface by default
   261  	var installObserver gadget.ContentObserver
   262  	trustedInstallObserver, err = boot.TrustedAssetsInstallObserverForModel(model, gadgetDir, useEncryption)
   263  	if err != nil && err != boot.ErrObserverNotApplicable {
   264  		return fmt.Errorf("cannot setup asset install observer: %v", err)
   265  	}
   266  	if err == nil {
   267  		installObserver = trustedInstallObserver
   268  		if !useEncryption {
   269  			// there will be no key sealing, so past the
   270  			// installation pass no other methods need to be called
   271  			trustedInstallObserver = nil
   272  		}
   273  	}
   274  
   275  	var installedSystem *install.InstalledSystemSideData
   276  	// run the create partition code
   277  	logger.Noticef("create and deploy partitions")
   278  	timings.Run(perfTimings, "install-run", "Install the run system", func(tm timings.Measurer) {
   279  		st.Unlock()
   280  		defer st.Lock()
   281  		installedSystem, err = installRun(model, gadgetDir, kernelDir, "", bopts, installObserver, tm)
   282  	})
   283  	if err != nil {
   284  		return fmt.Errorf("cannot install system: %v", err)
   285  	}
   286  
   287  	if trustedInstallObserver != nil {
   288  		// sanity check
   289  		if installedSystem.KeysForRoles == nil || installedSystem.KeysForRoles[gadget.SystemData] == nil || installedSystem.KeysForRoles[gadget.SystemSave] == nil {
   290  			return fmt.Errorf("internal error: system encryption keys are unset")
   291  		}
   292  		dataKeySet := installedSystem.KeysForRoles[gadget.SystemData]
   293  		saveKeySet := installedSystem.KeysForRoles[gadget.SystemSave]
   294  
   295  		// make note of the encryption keys
   296  		trustedInstallObserver.ChosenEncryptionKeys(dataKeySet.Key, saveKeySet.Key)
   297  
   298  		// keep track of recovery assets
   299  		if err := trustedInstallObserver.ObserveExistingTrustedRecoveryAssets(boot.InitramfsUbuntuSeedDir); err != nil {
   300  			return fmt.Errorf("cannot observe existing trusted recovery assets: err")
   301  		}
   302  		if err := saveKeys(installedSystem.KeysForRoles); err != nil {
   303  			return err
   304  		}
   305  		// write markers containing a secret to pair data and save
   306  		if err := writeMarkers(); err != nil {
   307  			return err
   308  		}
   309  	}
   310  
   311  	// keep track of the model we installed
   312  	err = os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir, "device"), 0755)
   313  	if err != nil {
   314  		return fmt.Errorf("cannot store the model: %v", err)
   315  	}
   316  	err = writeModel(model, filepath.Join(boot.InitramfsUbuntuBootDir, "device/model"))
   317  	if err != nil {
   318  		return fmt.Errorf("cannot store the model: %v", err)
   319  	}
   320  
   321  	// configure the run system
   322  	opts := &sysconfig.Options{TargetRootDir: boot.InstallHostWritableDir, GadgetDir: gadgetDir}
   323  	// configure cloud init
   324  	setSysconfigCloudOptions(opts, gadgetDir, model)
   325  	timings.Run(perfTimings, "sysconfig-configure-target-system", "Configure target system", func(timings.Measurer) {
   326  		err = sysconfigConfigureTargetSystem(model, opts)
   327  	})
   328  	if err != nil {
   329  		return err
   330  	}
   331  
   332  	// make it bootable
   333  	logger.Noticef("make system runnable")
   334  	bootBaseInfo, err := snapstate.BootBaseInfo(st, deviceCtx)
   335  	if err != nil {
   336  		return fmt.Errorf("cannot get boot base info: %v", err)
   337  	}
   338  	recoverySystemDir := filepath.Join("/systems", modeEnv.RecoverySystem)
   339  	bootWith := &boot.BootableSet{
   340  		Base:              bootBaseInfo,
   341  		BasePath:          bootBaseInfo.MountFile(),
   342  		Kernel:            kernelInfo,
   343  		KernelPath:        kernelInfo.MountFile(),
   344  		RecoverySystemDir: recoverySystemDir,
   345  		UnpackedGadgetDir: gadgetDir,
   346  	}
   347  	timings.Run(perfTimings, "boot-make-runnable", "Make target system runnable", func(timings.Measurer) {
   348  		err = bootMakeRunnable(deviceCtx.Model(), bootWith, trustedInstallObserver)
   349  	})
   350  	if err != nil {
   351  		return fmt.Errorf("cannot make system runnable: %v", err)
   352  	}
   353  
   354  	return nil
   355  }
   356  
   357  // writeMarkers writes markers containing the same secret to pair data and save.
   358  func writeMarkers() error {
   359  	// ensure directory for markers exists
   360  	if err := os.MkdirAll(boot.InstallHostFDEDataDir, 0755); err != nil {
   361  		return err
   362  	}
   363  	if err := os.MkdirAll(boot.InstallHostFDESaveDir, 0755); err != nil {
   364  		return err
   365  	}
   366  
   367  	// generate a secret random marker
   368  	markerSecret, err := randutil.CryptoTokenBytes(32)
   369  	if err != nil {
   370  		return fmt.Errorf("cannot create ubuntu-data/save marker secret: %v", err)
   371  	}
   372  
   373  	dataMarker := filepath.Join(boot.InstallHostFDEDataDir, "marker")
   374  	if err := osutil.AtomicWriteFile(dataMarker, markerSecret, 0600, 0); err != nil {
   375  		return err
   376  	}
   377  
   378  	saveMarker := filepath.Join(boot.InstallHostFDESaveDir, "marker")
   379  	if err := osutil.AtomicWriteFile(saveMarker, markerSecret, 0600, 0); err != nil {
   380  		return err
   381  	}
   382  
   383  	return nil
   384  }
   385  
   386  func saveKeys(keysForRoles map[string]*install.EncryptionKeySet) error {
   387  	dataKeySet := keysForRoles[gadget.SystemData]
   388  
   389  	// ensure directory for keys exists
   390  	if err := os.MkdirAll(boot.InstallHostFDEDataDir, 0755); err != nil {
   391  		return err
   392  	}
   393  
   394  	// Write the recovery key
   395  	recoveryKeyFile := filepath.Join(boot.InstallHostFDEDataDir, "recovery.key")
   396  	if err := dataKeySet.RecoveryKey.Save(recoveryKeyFile); err != nil {
   397  		return fmt.Errorf("cannot store recovery key: %v", err)
   398  	}
   399  
   400  	saveKeySet := keysForRoles[gadget.SystemSave]
   401  	if saveKeySet == nil {
   402  		// no system-save support
   403  		return nil
   404  	}
   405  
   406  	saveKey := filepath.Join(boot.InstallHostFDEDataDir, "ubuntu-save.key")
   407  	reinstallSaveKey := filepath.Join(boot.InstallHostFDEDataDir, "reinstall.key")
   408  
   409  	if err := saveKeySet.Key.Save(saveKey); err != nil {
   410  		return fmt.Errorf("cannot store system save key: %v", err)
   411  	}
   412  	if err := saveKeySet.RecoveryKey.Save(reinstallSaveKey); err != nil {
   413  		return fmt.Errorf("cannot store reinstall key: %v", err)
   414  	}
   415  	return nil
   416  }
   417  
   418  var secbootCheckTPMKeySealingSupported = secboot.CheckTPMKeySealingSupported
   419  
   420  // checkEncryption verifies whether encryption should be used based on the
   421  // model grade and the availability of a TPM device or a fde-setup hook
   422  // in the kernel.
   423  func (m *DeviceManager) checkEncryption(st *state.State, deviceCtx snapstate.DeviceContext) (res bool, err error) {
   424  	model := deviceCtx.Model()
   425  	secured := model.Grade() == asserts.ModelSecured
   426  	dangerous := model.Grade() == asserts.ModelDangerous
   427  	encrypted := model.StorageSafety() == asserts.StorageSafetyEncrypted
   428  
   429  	// check if we should disable encryption non-secured devices
   430  	// TODO:UC20: this is not the final mechanism to bypass encryption
   431  	if dangerous && osutil.FileExists(filepath.Join(boot.InitramfsUbuntuSeedDir, ".force-unencrypted")) {
   432  		return false, nil
   433  	}
   434  
   435  	// check if the model prefers to be unencrypted
   436  	// TODO: provide way to select via install chooser menu
   437  	//       if the install is unencrypted or encrypted
   438  	if model.StorageSafety() == asserts.StorageSafetyPreferUnencrypted {
   439  		logger.Noticef(`installing system unencrypted to comply with prefer-unencrypted storage-safety model option`)
   440  		return false, nil
   441  	}
   442  
   443  	// check if encryption is available
   444  	var (
   445  		hasFDESetupHook    bool
   446  		checkEncryptionErr error
   447  	)
   448  	if kernelInfo, err := snapstate.KernelInfo(st, deviceCtx); err == nil {
   449  		if hasFDESetupHook = hasFDESetupHookInKernel(kernelInfo); hasFDESetupHook {
   450  			checkEncryptionErr = m.checkFDEFeatures(st)
   451  		}
   452  	}
   453  	// Note that having a fde-setup hook will disable the build-in
   454  	// secboot encryption
   455  	if !hasFDESetupHook {
   456  		checkEncryptionErr = secbootCheckTPMKeySealingSupported()
   457  	}
   458  
   459  	// check if encryption is required
   460  	if checkEncryptionErr != nil {
   461  		if secured {
   462  			return false, fmt.Errorf("cannot encrypt device storage as mandated by model grade secured: %v", checkEncryptionErr)
   463  		}
   464  		if encrypted {
   465  			return false, fmt.Errorf("cannot encrypt device storage as mandated by encrypted storage-safety model option: %v", checkEncryptionErr)
   466  		}
   467  
   468  		if hasFDESetupHook {
   469  			logger.Noticef("not encrypting device storage as querying kernel fde-setup hook did not succeed: %v", checkEncryptionErr)
   470  		} else {
   471  			logger.Noticef("not encrypting device storage as checking TPM gave: %v", checkEncryptionErr)
   472  		}
   473  
   474  		// not required, go without
   475  		return false, nil
   476  	}
   477  
   478  	// encrypt
   479  	return true, nil
   480  }
   481  
   482  // RebootOptions can be attached to restart-system-to-run-mode tasks to control
   483  // their restart behavior.
   484  type RebootOptions struct {
   485  	Op string `json:"op,omitempty"`
   486  }
   487  
   488  const (
   489  	RebootHaltOp     = "halt"
   490  	RebootPoweroffOp = "poweroff"
   491  )
   492  
   493  func (m *DeviceManager) doRestartSystemToRunMode(t *state.Task, _ *tomb.Tomb) error {
   494  	st := t.State()
   495  	st.Lock()
   496  	defer st.Unlock()
   497  
   498  	perfTimings := state.TimingsForTask(t)
   499  	defer perfTimings.Save(st)
   500  
   501  	modeEnv, err := maybeReadModeenv()
   502  	if err != nil {
   503  		return err
   504  	}
   505  
   506  	if modeEnv == nil {
   507  		return fmt.Errorf("missing modeenv, cannot proceed")
   508  	}
   509  
   510  	// ensure the next boot goes into run mode
   511  	if err := bootEnsureNextBootToRunMode(modeEnv.RecoverySystem); err != nil {
   512  		return err
   513  	}
   514  
   515  	var rebootOpts RebootOptions
   516  	err = t.Get("reboot", &rebootOpts)
   517  	if err != nil && err != state.ErrNoState {
   518  		return err
   519  	}
   520  
   521  	// write timing information
   522  	if err := writeTimings(st, boot.InstallHostWritableDir); err != nil {
   523  		logger.Noticef("cannot write timings: %v", err)
   524  	}
   525  	// store install-mode log into ubuntu-data partition
   526  	if err := writeLogs(boot.InstallHostWritableDir); err != nil {
   527  		logger.Noticef("cannot write installation log: %v", err)
   528  	}
   529  
   530  	// request by default a restart as the last action after a
   531  	// successful install or what install-device requested via
   532  	// snapctl reboot
   533  	rst := state.RestartSystemNow
   534  	what := "restart"
   535  	switch rebootOpts.Op {
   536  	case RebootHaltOp:
   537  		what = "halt"
   538  		rst = state.RestartSystemHaltNow
   539  	case RebootPoweroffOp:
   540  		what = "poweroff"
   541  		rst = state.RestartSystemPoweroffNow
   542  	}
   543  	logger.Noticef("request immediate system %s", what)
   544  	st.RequestRestart(rst)
   545  
   546  	return nil
   547  }