gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/boot/boot.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019-2020 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 boot
    21  
    22  import (
    23  	"errors"
    24  	"fmt"
    25  
    26  	"github.com/snapcore/snapd/asserts"
    27  	"github.com/snapcore/snapd/bootloader"
    28  	"github.com/snapcore/snapd/snap"
    29  )
    30  
    31  const (
    32  	// DefaultStatus is the value of a status boot variable when nothing is
    33  	// being tried
    34  	DefaultStatus = ""
    35  	// TryStatus is the value of a status boot variable when something is about
    36  	// to be tried
    37  	TryStatus = "try"
    38  	// TryingStatus is the value of a status boot variable after we have
    39  	// attempted a boot with a try snap - this status is only set in the early
    40  	// boot sequence (bootloader, initramfs, etc.)
    41  	TryingStatus = "trying"
    42  )
    43  
    44  // A BootParticipant handles the boot process details for a snap involved in it.
    45  type BootParticipant interface {
    46  	// SetNextBoot will schedule the snap to be used in the next boot. For
    47  	// base snaps it is up to the caller to select the right bootable base
    48  	// (from the model assertion). It is a noop for not relevant snaps.
    49  	// Otherwise it returns whether a reboot is required.
    50  	SetNextBoot() (rebootRequired bool, err error)
    51  
    52  	// Is this a trivial implementation of the interface?
    53  	IsTrivial() bool
    54  }
    55  
    56  // A BootKernel handles the bootloader setup of a kernel.
    57  type BootKernel interface {
    58  	// RemoveKernelAssets removes the unpacked kernel/initrd for the given
    59  	// kernel snap.
    60  	RemoveKernelAssets() error
    61  	// ExtractKernelAssets extracts kernel/initrd/dtb data from the given
    62  	// kernel snap, if required, to a versioned bootloader directory so
    63  	// that the bootloader can use it.
    64  	ExtractKernelAssets(snap.Container) error
    65  	// Is this a trivial implementation of the interface?
    66  	IsTrivial() bool
    67  }
    68  
    69  type trivial struct{}
    70  
    71  func (trivial) SetNextBoot() (bool, error)               { return false, nil }
    72  func (trivial) IsTrivial() bool                          { return true }
    73  func (trivial) RemoveKernelAssets() error                { return nil }
    74  func (trivial) ExtractKernelAssets(snap.Container) error { return nil }
    75  
    76  // ensure trivial is a BootParticipant
    77  var _ BootParticipant = trivial{}
    78  
    79  // ensure trivial is a Kernel
    80  var _ BootKernel = trivial{}
    81  
    82  // Device carries information about the device model and mode that is
    83  // relevant to boot. Note snapstate.DeviceContext implements this, and that's
    84  // the expected use case.
    85  type Device interface {
    86  	RunMode() bool
    87  	Classic() bool
    88  
    89  	Kernel() string
    90  	Base() string
    91  
    92  	HasModeenv() bool
    93  
    94  	Model() *asserts.Model
    95  }
    96  
    97  // Participant figures out what the BootParticipant is for the given
    98  // arguments, and returns it. If the snap does _not_ participate in
    99  // the boot process, the returned object will be a NOP, so it's safe
   100  // to call anything on it always.
   101  //
   102  // Currently, on classic, nothing is a boot participant (returned will
   103  // always be NOP).
   104  func Participant(s snap.PlaceInfo, t snap.Type, dev Device) BootParticipant {
   105  	if applicable(s, t, dev) {
   106  		bs, err := bootStateFor(t, dev)
   107  		if err != nil {
   108  			// all internal errors at this point
   109  			panic(err)
   110  		}
   111  		return &coreBootParticipant{s: s, bs: bs}
   112  	}
   113  	return trivial{}
   114  }
   115  
   116  // bootloaderOptionsForDeviceKernel returns a set of bootloader options that
   117  // enable correct kernel extraction and removal for given device
   118  func bootloaderOptionsForDeviceKernel(dev Device) *bootloader.Options {
   119  	if !dev.HasModeenv() {
   120  		return nil
   121  	}
   122  	// find the run-mode bootloader with its kernel support for UC20
   123  	return &bootloader.Options{
   124  		Role: bootloader.RoleRunMode,
   125  	}
   126  }
   127  
   128  // Kernel checks that the given arguments refer to a kernel snap
   129  // that participates in the boot process, and returns the associated
   130  // BootKernel, or a trivial implementation otherwise.
   131  func Kernel(s snap.PlaceInfo, t snap.Type, dev Device) BootKernel {
   132  	if t == snap.TypeKernel && applicable(s, t, dev) {
   133  		return &coreKernel{s: s, bopts: bootloaderOptionsForDeviceKernel(dev)}
   134  	}
   135  	return trivial{}
   136  }
   137  
   138  func applicable(s snap.PlaceInfo, t snap.Type, dev Device) bool {
   139  	if dev.Classic() {
   140  		return false
   141  	}
   142  	// In ephemeral modes we never need to care about updating the boot
   143  	// config. This will be done via boot.MakeBootable().
   144  	if !dev.RunMode() {
   145  		return false
   146  	}
   147  
   148  	if t != snap.TypeOS && t != snap.TypeKernel && t != snap.TypeBase {
   149  		// note we don't currently have anything useful to do with gadgets
   150  		return false
   151  	}
   152  
   153  	switch t {
   154  	case snap.TypeKernel:
   155  		if s.InstanceName() != dev.Kernel() {
   156  			// a remodel might leave you in this state
   157  			return false
   158  		}
   159  	case snap.TypeBase, snap.TypeOS:
   160  		base := dev.Base()
   161  		if base == "" {
   162  			base = "core"
   163  		}
   164  		if s.InstanceName() != base {
   165  			return false
   166  		}
   167  	}
   168  
   169  	return true
   170  }
   171  
   172  // bootState exposes the boot state for a type of boot snap during
   173  // normal running state, i.e. after the pivot_root and after the initramfs.
   174  type bootState interface {
   175  	// revisions retrieves the revisions of the current snap and
   176  	// the try snap (only the latter might not be set), and
   177  	// the status of the trying snap.
   178  	// Note that the error could be only specific to the try snap, in which case
   179  	// curSnap may still be non-nil and valid. Callers concerned with robustness
   180  	// should always inspect a non-nil error with isTrySnapError, and use
   181  	// curSnap instead if the error is only for the trySnap or tryingStatus.
   182  	revisions() (curSnap, trySnap snap.PlaceInfo, tryingStatus string, err error)
   183  
   184  	// setNext lazily implements setting the next boot target for
   185  	// the type's boot snap. actually committing the update
   186  	// is done via the returned bootStateUpdate's commit method.
   187  	setNext(s snap.PlaceInfo) (rebootRequired bool, u bootStateUpdate, err error)
   188  
   189  	// markSuccessful lazily implements marking the boot
   190  	// successful for the type's boot snap. The actual committing
   191  	// of the update is done via bootStateUpdate's commit, that
   192  	// way different markSuccessful can be folded together.
   193  	markSuccessful(bootStateUpdate) (bootStateUpdate, error)
   194  }
   195  
   196  // successfulBootState exposes the state of resources requiring bookkeeping on a
   197  // successful boot.
   198  type successfulBootState interface {
   199  	// markSuccessful lazily implements marking the boot
   200  	// successful for the given type of resource.
   201  	markSuccessful(bootStateUpdate) (bootStateUpdate, error)
   202  }
   203  
   204  // bootStateFor finds the right bootState implementation of the given
   205  // snap type and Device, if applicable.
   206  func bootStateFor(typ snap.Type, dev Device) (s bootState, err error) {
   207  	if !dev.RunMode() {
   208  		return nil, fmt.Errorf("internal error: no boot state handling for ephemeral modes")
   209  	}
   210  	newBootState := newBootState16
   211  	if dev.HasModeenv() {
   212  		newBootState = newBootState20
   213  	}
   214  	switch typ {
   215  	case snap.TypeOS, snap.TypeBase:
   216  		return newBootState(snap.TypeBase, dev), nil
   217  	case snap.TypeKernel:
   218  		return newBootState(snap.TypeKernel, dev), nil
   219  	default:
   220  		return nil, fmt.Errorf("internal error: no boot state handling for snap type %q", typ)
   221  	}
   222  }
   223  
   224  // InUseFunc is a function to check if the snap is in use or not.
   225  type InUseFunc func(name string, rev snap.Revision) bool
   226  
   227  func fixedInUse(inUse bool) InUseFunc {
   228  	return func(string, snap.Revision) bool {
   229  		return inUse
   230  	}
   231  }
   232  
   233  // InUse returns a checker for whether a given name/revision is used in the
   234  // boot environment for snaps of the relevant snap type.
   235  func InUse(typ snap.Type, dev Device) (InUseFunc, error) {
   236  	if dev.Classic() {
   237  		// no boot state on classic
   238  		return fixedInUse(false), nil
   239  	}
   240  	if !dev.RunMode() {
   241  		// ephemeral mode, block manipulations for now
   242  		return fixedInUse(true), nil
   243  	}
   244  	switch typ {
   245  	case snap.TypeKernel, snap.TypeBase, snap.TypeOS:
   246  		break
   247  	default:
   248  		return fixedInUse(false), nil
   249  	}
   250  	cands := make([]snap.PlaceInfo, 0, 2)
   251  	s, err := bootStateFor(typ, dev)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  	cand, tryCand, _, err := s.revisions()
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  	cands = append(cands, cand)
   260  	if tryCand != nil {
   261  		cands = append(cands, tryCand)
   262  	}
   263  
   264  	return func(name string, rev snap.Revision) bool {
   265  		for _, cand := range cands {
   266  			if cand.SnapName() == name && cand.SnapRevision() == rev {
   267  				return true
   268  			}
   269  		}
   270  		return false
   271  	}, nil
   272  }
   273  
   274  var (
   275  	// ErrBootNameAndRevisionNotReady is returned when the boot revision is not
   276  	// established yet.
   277  	ErrBootNameAndRevisionNotReady = errors.New("boot revision not yet established")
   278  )
   279  
   280  // GetCurrentBoot returns the currently set name and revision for boot for the given
   281  // type of snap, which can be snap.TypeBase (or snap.TypeOS), or snap.TypeKernel.
   282  // Returns ErrBootNameAndRevisionNotReady if the values are temporarily not established.
   283  func GetCurrentBoot(t snap.Type, dev Device) (snap.PlaceInfo, error) {
   284  	s, err := bootStateFor(t, dev)
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  
   289  	snap, _, status, err := s.revisions()
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  
   294  	if status == TryingStatus {
   295  		return nil, ErrBootNameAndRevisionNotReady
   296  	}
   297  
   298  	return snap, nil
   299  }
   300  
   301  // bootStateUpdate carries the state for an on-going boot state update.
   302  // At the end it can be used to commit it.
   303  type bootStateUpdate interface {
   304  	commit() error
   305  }
   306  
   307  // MarkBootSuccessful marks the current boot as successful. This means
   308  // that snappy will consider this combination of kernel/os a valid
   309  // target for rollback.
   310  //
   311  // The states that a boot goes through for UC16/18 are the following:
   312  // - By default snap_mode is "" in which case the bootloader loads
   313  //   two squashfs'es denoted by variables snap_core and snap_kernel.
   314  // - On a refresh of core/kernel snapd will set snap_mode=try and
   315  //   will also set snap_try_{core,kernel} to the core/kernel that
   316  //   will be tried next.
   317  // - On reboot the bootloader will inspect the snap_mode and if the
   318  //   mode is set to "try" it will set "snap_mode=trying" and then
   319  //   try to boot the snap_try_{core,kernel}".
   320  // - On a successful boot snapd resets snap_mode to "" and copies
   321  //   snap_try_{core,kernel} to snap_{core,kernel}. The snap_try_*
   322  //   values are cleared afterwards.
   323  // - On a failing boot the bootloader will see snap_mode=trying which
   324  //   means snapd did not start successfully. In this case the bootloader
   325  //   will set snap_mode="" and the system will boot with the known good
   326  //   values from snap_{core,kernel}
   327  func MarkBootSuccessful(dev Device) error {
   328  	const errPrefix = "cannot mark boot successful: %s"
   329  
   330  	var u bootStateUpdate
   331  	for _, t := range []snap.Type{snap.TypeBase, snap.TypeKernel} {
   332  		s, err := bootStateFor(t, dev)
   333  		if err != nil {
   334  			return err
   335  		}
   336  		u, err = s.markSuccessful(u)
   337  		if err != nil {
   338  			return fmt.Errorf(errPrefix, err)
   339  		}
   340  	}
   341  
   342  	if dev.HasModeenv() {
   343  		for _, bs := range []successfulBootState{
   344  			trustedAssetsBootState(dev),
   345  			trustedCommandLineBootState(dev),
   346  			recoverySystemsBootState(dev),
   347  			modelBootState(dev),
   348  		} {
   349  			var err error
   350  			u, err = bs.markSuccessful(u)
   351  			if err != nil {
   352  				return fmt.Errorf(errPrefix, err)
   353  			}
   354  		}
   355  	}
   356  
   357  	if u != nil {
   358  		if err := u.commit(); err != nil {
   359  			return fmt.Errorf(errPrefix, err)
   360  		}
   361  	}
   362  	return nil
   363  }
   364  
   365  var ErrUnsupportedSystemMode = errors.New("system mode is unsupported")
   366  
   367  // SetRecoveryBootSystemAndMode configures the recovery bootloader to boot into
   368  // the given recovery system in a particular mode. Returns
   369  // ErrUnsupportedSystemMode when booting into a recovery system is not supported
   370  // by the device.
   371  func SetRecoveryBootSystemAndMode(dev Device, systemLabel, mode string) error {
   372  	if !dev.HasModeenv() {
   373  		// only UC20 devices are supported
   374  		return ErrUnsupportedSystemMode
   375  	}
   376  	if systemLabel == "" {
   377  		return fmt.Errorf("internal error: system label is unset")
   378  	}
   379  	if mode == "" {
   380  		return fmt.Errorf("internal error: system mode is unset")
   381  	}
   382  
   383  	opts := &bootloader.Options{
   384  		// setup the recovery bootloader
   385  		Role: bootloader.RoleRecovery,
   386  	}
   387  	// TODO:UC20: should the recovery partition stay around as RW during run
   388  	// mode all the time?
   389  	bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts)
   390  	if err != nil {
   391  		return err
   392  	}
   393  
   394  	m := map[string]string{
   395  		"snapd_recovery_system": systemLabel,
   396  		"snapd_recovery_mode":   mode,
   397  	}
   398  	return bl.SetBootVars(m)
   399  }
   400  
   401  // UpdateManagedBootConfigs updates managed boot config assets if those are
   402  // present for the ubuntu-boot bootloader. Returns true when an update was
   403  // carried out.
   404  func UpdateManagedBootConfigs(dev Device, gadgetSnapOrDir string) (updated bool, err error) {
   405  	if !dev.HasModeenv() {
   406  		// only UC20 devices use managed boot config
   407  		return false, nil
   408  	}
   409  	if !dev.RunMode() {
   410  		return false, fmt.Errorf("internal error: boot config can only be updated in run mode")
   411  	}
   412  	return updateManagedBootConfigForBootloader(dev, ModeRun, gadgetSnapOrDir)
   413  }
   414  
   415  func updateManagedBootConfigForBootloader(dev Device, mode, gadgetSnapOrDir string) (updated bool, err error) {
   416  	if mode != ModeRun {
   417  		return false, fmt.Errorf("internal error: updating boot config of recovery bootloader is not supported yet")
   418  	}
   419  
   420  	opts := &bootloader.Options{
   421  		Role:        bootloader.RoleRunMode,
   422  		NoSlashBoot: true,
   423  	}
   424  	tbl, err := getBootloaderManagingItsAssets(InitramfsUbuntuBootDir, opts)
   425  	if err != nil {
   426  		if err == errBootConfigNotManaged {
   427  			// we're not managing this bootloader's boot config
   428  			return false, nil
   429  		}
   430  		return false, err
   431  	}
   432  	// boot config update can lead to a change of kernel command line
   433  	_, err = observeCommandLineUpdate(dev.Model(), commandLineUpdateReasonSnapd, gadgetSnapOrDir)
   434  	if err != nil {
   435  		return false, err
   436  	}
   437  	return tbl.UpdateBootConfig()
   438  }
   439  
   440  // UpdateCommandLineForGadgetComponent handles the update of a gadget that
   441  // contributes to the kernel command line of the run system. Returns true when a
   442  // change in command line has been observed and a reboot is needed. The reboot,
   443  // if needed, should be requested at the the earliest possible occasion.
   444  func UpdateCommandLineForGadgetComponent(dev Device, gadgetSnapOrDir string) (needsReboot bool, err error) {
   445  	if !dev.HasModeenv() {
   446  		// only UC20 devices are supported
   447  		return false, fmt.Errorf("internal error: command line component cannot be updated on non UC20 devices")
   448  	}
   449  	opts := &bootloader.Options{
   450  		Role: bootloader.RoleRunMode,
   451  	}
   452  	// TODO: add support for bootloaders that that do not have any managed
   453  	// assets
   454  	tbl, err := getBootloaderManagingItsAssets("", opts)
   455  	if err != nil {
   456  		if err == errBootConfigNotManaged {
   457  			// we're not managing this bootloader's boot config
   458  			return false, nil
   459  		}
   460  		return false, err
   461  	}
   462  	// gadget update can lead to a change of kernel command line
   463  	cmdlineChange, err := observeCommandLineUpdate(dev.Model(), commandLineUpdateReasonGadget, gadgetSnapOrDir)
   464  	if err != nil {
   465  		return false, err
   466  	}
   467  	if !cmdlineChange {
   468  		return false, nil
   469  	}
   470  	// update the bootloader environment, maybe clearing the relevant
   471  	// variables
   472  	cmdlineVars, err := bootVarsForTrustedCommandLineFromGadget(gadgetSnapOrDir)
   473  	if err != nil {
   474  		return false, fmt.Errorf("cannot prepare bootloader variables for kernel command line: %v", err)
   475  	}
   476  	if err := tbl.SetBootVars(cmdlineVars); err != nil {
   477  		return false, fmt.Errorf("cannot set run system kernel command line arguments: %v", err)
   478  	}
   479  	return cmdlineChange, nil
   480  }