github.com/rigado/snapd@v2.42.5-go-mod+incompatible/boot/boot.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019 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  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  
    29  	"github.com/snapcore/snapd/asserts"
    30  	"github.com/snapcore/snapd/bootloader"
    31  	"github.com/snapcore/snapd/dirs"
    32  	"github.com/snapcore/snapd/logger"
    33  	"github.com/snapcore/snapd/snap"
    34  )
    35  
    36  // A BootParticipant handles the boot process details for a snap involved in it.
    37  type BootParticipant interface {
    38  	// SetNextBoot will schedule the snap to be used in the next boot. For
    39  	// base snaps it is up to the caller to select the right bootable base
    40  	// (from the model assertion).
    41  	SetNextBoot() error
    42  	// ChangeRequiresReboot returns whether a reboot is required to switch
    43  	// to the snap.
    44  	ChangeRequiresReboot() bool
    45  	// Is this a trivial implementation of the interface?
    46  	IsTrivial() bool
    47  }
    48  
    49  // A BootKernel handles the bootloader setup of a kernel.
    50  type BootKernel interface {
    51  	// RemoveKernelAssets removes the unpacked kernel/initrd for the given
    52  	// kernel snap.
    53  	RemoveKernelAssets() error
    54  	// ExtractKernelAssets extracts kernel/initrd/dtb data from the given
    55  	// kernel snap, if required, to a versioned bootloader directory so
    56  	// that the bootloader can use it.
    57  	ExtractKernelAssets(snap.Container) error
    58  	// Is this a trivial implementation of the interface?
    59  	IsTrivial() bool
    60  }
    61  
    62  type trivial struct{}
    63  
    64  func (trivial) SetNextBoot() error                       { return nil }
    65  func (trivial) ChangeRequiresReboot() bool               { return false }
    66  func (trivial) IsTrivial() bool                          { return true }
    67  func (trivial) RemoveKernelAssets() error                { return nil }
    68  func (trivial) ExtractKernelAssets(snap.Container) error { return nil }
    69  
    70  // ensure trivial is a BootParticipant
    71  var _ BootParticipant = trivial{}
    72  
    73  // ensure trivial is a Kernel
    74  var _ BootKernel = trivial{}
    75  
    76  // Model carries information about the model that is relevant to boot.
    77  // Note *asserts.Model implements this, and that's the expected use case.
    78  type Model interface {
    79  	Kernel() string
    80  	Base() string
    81  	Classic() bool
    82  }
    83  
    84  // Participant figures out what the BootParticipant is for the given
    85  // arguments, and returns it. If the snap does _not_ participate in
    86  // the boot process, the returned object will be a NOP, so it's safe
    87  // to call anything on it always.
    88  //
    89  // Currently, on classic, nothing is a boot participant (returned will
    90  // always be NOP).
    91  func Participant(s snap.PlaceInfo, t snap.Type, model Model, onClassic bool) BootParticipant {
    92  	if applicable(s, t, model, onClassic) {
    93  		return &coreBootParticipant{s: s, t: t}
    94  	}
    95  	return trivial{}
    96  }
    97  
    98  // Kernel checks that the given arguments refer to a kernel snap
    99  // that participates in the boot process, and returns the associated
   100  // BootKernel, or a trivial implementation otherwise.
   101  func Kernel(s snap.PlaceInfo, t snap.Type, model Model, onClassic bool) BootKernel {
   102  	if t == snap.TypeKernel && applicable(s, t, model, onClassic) {
   103  		return &coreKernel{s: s}
   104  	}
   105  	return trivial{}
   106  }
   107  
   108  func applicable(s snap.PlaceInfo, t snap.Type, model Model, onClassic bool) bool {
   109  	if onClassic {
   110  		return false
   111  	}
   112  	if t != snap.TypeOS && t != snap.TypeKernel && t != snap.TypeBase {
   113  		// note we don't currently have anything useful to do with gadgets
   114  		return false
   115  	}
   116  
   117  	if model != nil {
   118  		switch t {
   119  		case snap.TypeKernel:
   120  			if s.InstanceName() != model.Kernel() {
   121  				// a remodel might leave you in this state
   122  				return false
   123  			}
   124  		case snap.TypeBase, snap.TypeOS:
   125  			base := model.Base()
   126  			if base == "" {
   127  				base = "core"
   128  			}
   129  			if s.InstanceName() != base {
   130  				return false
   131  			}
   132  		}
   133  	}
   134  
   135  	return true
   136  }
   137  
   138  // InUse checks if the given name/revision is used in the
   139  // boot environment
   140  func InUse(name string, rev snap.Revision) bool {
   141  	bootloader, err := bootloader.Find("", nil)
   142  	if err != nil {
   143  		logger.Noticef("cannot get boot settings: %s", err)
   144  		return false
   145  	}
   146  
   147  	bootVars, err := bootloader.GetBootVars("snap_kernel", "snap_try_kernel", "snap_core", "snap_try_core")
   148  	if err != nil {
   149  		logger.Noticef("cannot get boot vars: %s", err)
   150  		return false
   151  	}
   152  
   153  	snapFile := filepath.Base(snap.MountFile(name, rev))
   154  	for _, bootVar := range bootVars {
   155  		if bootVar == snapFile {
   156  			return true
   157  		}
   158  	}
   159  
   160  	return false
   161  }
   162  
   163  var (
   164  	ErrBootNameAndRevisionNotReady = errors.New("boot revision not yet established")
   165  )
   166  
   167  type NameAndRevision struct {
   168  	Name     string
   169  	Revision snap.Revision
   170  }
   171  
   172  // GetCurrentBoot returns the currently set name and revision for boot for the given
   173  // type of snap, which can be snap.TypeBase (or snap.TypeOS), or snap.TypeKernel.
   174  // Returns ErrBootNameAndRevisionNotReady if the values are temporarily not established.
   175  func GetCurrentBoot(t snap.Type) (*NameAndRevision, error) {
   176  	var bootVar, errName string
   177  	switch t {
   178  	case snap.TypeKernel:
   179  		bootVar = "snap_kernel"
   180  		errName = "kernel"
   181  	case snap.TypeOS, snap.TypeBase:
   182  		bootVar = "snap_core"
   183  		errName = "base"
   184  	default:
   185  		return nil, fmt.Errorf("internal error: cannot find boot revision for snap type %q", t)
   186  	}
   187  
   188  	bloader, err := bootloader.Find("", nil)
   189  	if err != nil {
   190  		return nil, fmt.Errorf("cannot get boot settings: %s", err)
   191  	}
   192  
   193  	m, err := bloader.GetBootVars(bootVar, "snap_mode")
   194  	if err != nil {
   195  		return nil, fmt.Errorf("cannot get boot variables: %s", err)
   196  	}
   197  
   198  	if m["snap_mode"] == "trying" {
   199  		return nil, ErrBootNameAndRevisionNotReady
   200  	}
   201  
   202  	nameAndRevno, err := nameAndRevnoFromSnap(m[bootVar])
   203  	if err != nil {
   204  		return nil, fmt.Errorf("cannot get name and revision of boot %s: %v", errName, err)
   205  	}
   206  
   207  	return nameAndRevno, nil
   208  }
   209  
   210  // nameAndRevnoFromSnap grabs the snap name and revision from the
   211  // value of a boot variable. E.g., foo_2.snap -> name "foo", revno 2
   212  func nameAndRevnoFromSnap(sn string) (*NameAndRevision, error) {
   213  	if sn == "" {
   214  		return nil, fmt.Errorf("boot variable unset")
   215  	}
   216  	idx := strings.IndexByte(sn, '_')
   217  	if idx < 1 {
   218  		return nil, fmt.Errorf("input %q has invalid format (not enough '_')", sn)
   219  	}
   220  	name := sn[:idx]
   221  	revnoNSuffix := sn[idx+1:]
   222  	rev, err := snap.ParseRevision(strings.TrimSuffix(revnoNSuffix, ".snap"))
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	return &NameAndRevision{Name: name, Revision: rev}, nil
   227  }
   228  
   229  // MarkBootSuccessful marks the current boot as successful. This means
   230  // that snappy will consider this combination of kernel/os a valid
   231  // target for rollback.
   232  //
   233  // The states that a boot goes through are the following:
   234  // - By default snap_mode is "" in which case the bootloader loads
   235  //   two squashfs'es denoted by variables snap_core and snap_kernel.
   236  // - On a refresh of core/kernel snapd will set snap_mode=try and
   237  //   will also set snap_try_{core,kernel} to the core/kernel that
   238  //   will be tried next.
   239  // - On reboot the bootloader will inspect the snap_mode and if the
   240  //   mode is set to "try" it will set "snap_mode=trying" and then
   241  //   try to boot the snap_try_{core,kernel}".
   242  // - On a successful boot snapd resets snap_mode to "" and copies
   243  //   snap_try_{core,kernel} to snap_{core,kernel}. The snap_try_*
   244  //   values are cleared afterwards.
   245  // - On a failing boot the bootloader will see snap_mode=trying which
   246  //   means snapd did not start successfully. In this case the bootloader
   247  //   will set snap_mode="" and the system will boot with the known good
   248  //   values from snap_{core,kernel}
   249  func MarkBootSuccessful() error {
   250  	bl, err := bootloader.Find("", nil)
   251  	if err != nil {
   252  		return fmt.Errorf("cannot mark boot successful: %s", err)
   253  	}
   254  	m, err := bl.GetBootVars("snap_mode", "snap_try_core", "snap_try_kernel")
   255  	if err != nil {
   256  		return err
   257  	}
   258  
   259  	// snap_mode goes from "" -> "try" -> "trying" -> ""
   260  	// so if we are not in "trying" mode, nothing to do here
   261  	if m["snap_mode"] != "trying" {
   262  		return nil
   263  	}
   264  
   265  	// update the boot vars
   266  	for _, k := range []string{"kernel", "core"} {
   267  		tryBootVar := fmt.Sprintf("snap_try_%s", k)
   268  		bootVar := fmt.Sprintf("snap_%s", k)
   269  		// update the boot vars
   270  		if m[tryBootVar] != "" {
   271  			m[bootVar] = m[tryBootVar]
   272  			m[tryBootVar] = ""
   273  		}
   274  	}
   275  	m["snap_mode"] = ""
   276  
   277  	return bl.SetBootVars(m)
   278  }
   279  
   280  // MakeBootable sets up the image filesystem with the given rootdir
   281  // such that it can be booted. bootWith is a map from the paths in the
   282  // image seed for kernel and boot base to their snap info. This
   283  // entails:
   284  //  - installing the bootloader configuration from the gadget
   285  //  - creating symlinks for boot snaps from seed to the runtime blob dir
   286  //  - setting boot env vars pointing to the revisions of the boot snaps to use
   287  //  - extracting kernel assets as needed by the bootloader
   288  func MakeBootable(model *asserts.Model, rootdir string, bootWith map[string]*snap.Info, unpackedGadgetDir string) error {
   289  	if len(bootWith) != 2 {
   290  		return fmt.Errorf("internal error: MakeBootable can only be called with exactly one kernel and exactly one core/base boot info: %v", bootWith)
   291  	}
   292  
   293  	// install the bootloader configuration from the gadget
   294  	if err := bootloader.InstallBootConfig(unpackedGadgetDir, rootdir); err != nil {
   295  		return err
   296  	}
   297  
   298  	// setup symlinks for kernel and boot base from the blob directory
   299  	// to the seed snaps
   300  
   301  	snapBlobDir := dirs.SnapBlobDirUnder(rootdir)
   302  	if err := os.MkdirAll(snapBlobDir, 0755); err != nil {
   303  		return err
   304  	}
   305  
   306  	for fn := range bootWith {
   307  		dst := filepath.Join(snapBlobDir, filepath.Base(fn))
   308  		// construct a relative symlink from the blob dir
   309  		// to the seed snap file
   310  		relSymlink, err := filepath.Rel(snapBlobDir, fn)
   311  		if err != nil {
   312  			return fmt.Errorf("cannot build symlink for boot snap: %v", err)
   313  		}
   314  		if err := os.Symlink(relSymlink, dst); err != nil {
   315  			return err
   316  		}
   317  	}
   318  
   319  	// Set bootvars for kernel/core snaps so the system boots and
   320  	// does the first-time initialization. There is also no
   321  	// mounted kernel/core/base snap, but just the blobs.
   322  	bl, err := bootloader.Find(rootdir, &bootloader.Options{
   323  		PrepareImageTime: true,
   324  	})
   325  	if err != nil {
   326  		return fmt.Errorf("cannot set kernel/core boot variables: %s", err)
   327  	}
   328  
   329  	m := map[string]string{
   330  		"snap_mode":       "",
   331  		"snap_try_core":   "",
   332  		"snap_try_kernel": "",
   333  	}
   334  	if model.DisplayName() != "" {
   335  		m["snap_menuentry"] = model.DisplayName()
   336  	}
   337  
   338  	for fn, info := range bootWith {
   339  		bootvar := ""
   340  
   341  		switch info.GetType() {
   342  		case snap.TypeOS, snap.TypeBase:
   343  			bootvar = "snap_core"
   344  		case snap.TypeKernel:
   345  			snapf, err := snap.Open(fn)
   346  			if err != nil {
   347  				return err
   348  			}
   349  
   350  			if err := bl.ExtractKernelAssets(info, snapf); err != nil {
   351  				return err
   352  			}
   353  			bootvar = "snap_kernel"
   354  		}
   355  
   356  		if bootvar != "" {
   357  			name := filepath.Base(fn)
   358  			m[bootvar] = name
   359  		}
   360  	}
   361  	if err := bl.SetBootVars(m); err != nil {
   362  		return err
   363  	}
   364  
   365  	return nil
   366  
   367  }