github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/overlord/devicestate/systems.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 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 devicestate
    21  
    22  import (
    23  	"fmt"
    24  	"path/filepath"
    25  	"strings"
    26  
    27  	"github.com/snapcore/snapd/asserts"
    28  	"github.com/snapcore/snapd/boot"
    29  	"github.com/snapcore/snapd/dirs"
    30  	"github.com/snapcore/snapd/logger"
    31  	"github.com/snapcore/snapd/osutil"
    32  	"github.com/snapcore/snapd/overlord/snapstate"
    33  	"github.com/snapcore/snapd/overlord/state"
    34  	"github.com/snapcore/snapd/seed"
    35  	"github.com/snapcore/snapd/seed/seedwriter"
    36  	"github.com/snapcore/snapd/snap"
    37  	"github.com/snapcore/snapd/strutil"
    38  )
    39  
    40  func checkSystemRequestConflict(st *state.State, systemLabel string) error {
    41  	st.Lock()
    42  	defer st.Unlock()
    43  
    44  	var seeded bool
    45  	if err := st.Get("seeded", &seeded); err != nil && err != state.ErrNoState {
    46  		return err
    47  	}
    48  	if seeded {
    49  		// the system is fully seeded already
    50  		return nil
    51  	}
    52  
    53  	// inspect the current system which is stored in modeenv, note we are
    54  	// holding the state lock so there is no race against mark-seeded
    55  	// clearing recovery system; recovery system is not cleared when seeding
    56  	// fails
    57  	modeEnv, err := maybeReadModeenv()
    58  	if err != nil {
    59  		return err
    60  	}
    61  	if modeEnv == nil {
    62  		// non UC20 systems do not support actions, no conflict can
    63  		// happen
    64  		return nil
    65  	}
    66  
    67  	// not yet fully seeded, hold off requests for the system that is being
    68  	// seeded, but allow requests for other systems
    69  	if modeEnv.RecoverySystem == systemLabel {
    70  		return &snapstate.ChangeConflictError{
    71  			ChangeKind: "seed",
    72  			Message:    "cannot request system action, system is seeding",
    73  		}
    74  	}
    75  	return nil
    76  }
    77  
    78  func systemFromSeed(label string, current *currentSystem) (*System, error) {
    79  	s, err := seed.Open(dirs.SnapSeedDir, label)
    80  	if err != nil {
    81  		return nil, fmt.Errorf("cannot open: %v", err)
    82  	}
    83  	if err := s.LoadAssertions(nil, nil); err != nil {
    84  		return nil, fmt.Errorf("cannot load assertions: %v", err)
    85  	}
    86  	// get the model
    87  	model := s.Model()
    88  	brand, err := s.Brand()
    89  	if err != nil {
    90  		return nil, fmt.Errorf("cannot obtain brand: %v", err)
    91  	}
    92  	system := &System{
    93  		Current: false,
    94  		Label:   label,
    95  		Model:   model,
    96  		Brand:   brand,
    97  		Actions: defaultSystemActions,
    98  	}
    99  	if current.sameAs(system) {
   100  		system.Current = true
   101  		system.Actions = current.actions
   102  	}
   103  	return system, nil
   104  }
   105  
   106  type currentSystem struct {
   107  	*seededSystem
   108  	actions []SystemAction
   109  }
   110  
   111  func (c *currentSystem) sameAs(other *System) bool {
   112  	return c != nil &&
   113  		c.System == other.Label &&
   114  		c.Model == other.Model.Model() &&
   115  		c.BrandID == other.Brand.AccountID()
   116  }
   117  
   118  func currentSystemForMode(st *state.State, mode string) (*currentSystem, error) {
   119  	var system *seededSystem
   120  	var actions []SystemAction
   121  	var err error
   122  
   123  	switch mode {
   124  	case "run":
   125  		actions = currentSystemActions
   126  		system, err = currentSeededSystem(st)
   127  	case "install":
   128  		// there is no current system for install mode
   129  		return nil, nil
   130  	case "recover":
   131  		actions = recoverSystemActions
   132  		// recover mode uses modeenv for reference
   133  		system, err = seededSystemFromModeenv()
   134  	default:
   135  		return nil, fmt.Errorf("internal error: cannot identify current system for unsupported mode %q", mode)
   136  	}
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	currentSys := &currentSystem{
   141  		seededSystem: system,
   142  		actions:      actions,
   143  	}
   144  	return currentSys, nil
   145  }
   146  
   147  func currentSeededSystem(st *state.State) (*seededSystem, error) {
   148  	st.Lock()
   149  	defer st.Unlock()
   150  
   151  	var whatseeded []seededSystem
   152  	if err := st.Get("seeded-systems", &whatseeded); err != nil {
   153  		return nil, err
   154  	}
   155  	if len(whatseeded) == 0 {
   156  		// unexpected
   157  		return nil, state.ErrNoState
   158  	}
   159  	// seeded systems are prepended to the list, so the most recently seeded
   160  	// one comes first
   161  	return &whatseeded[0], nil
   162  }
   163  
   164  func seededSystemFromModeenv() (*seededSystem, error) {
   165  	modeEnv, err := maybeReadModeenv()
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  	if modeEnv == nil {
   170  		return nil, fmt.Errorf("internal error: modeenv does not exist")
   171  	}
   172  	if modeEnv.RecoverySystem == "" {
   173  		return nil, fmt.Errorf("internal error: recovery system is unset")
   174  	}
   175  
   176  	system, err := systemFromSeed(modeEnv.RecoverySystem, nil)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  	seededSys := &seededSystem{
   181  		System:    modeEnv.RecoverySystem,
   182  		Model:     system.Model.Model(),
   183  		BrandID:   system.Model.BrandID(),
   184  		Revision:  system.Model.Revision(),
   185  		Timestamp: system.Model.Timestamp(),
   186  		// SeedTime is intentionally left unset
   187  	}
   188  	return seededSys, nil
   189  }
   190  
   191  // getInfoFunc is expected to return for a given snap name a snap.Info for that
   192  // snap and whether the snap is present is present. The second bit is relevant
   193  // for non-essential snaps mentioned in the model, which if present and having
   194  // an 'optional' presence in the model, will be added to the recovery system.
   195  type getSnapInfoFunc func(name string) (info *snap.Info, snapIsPresent bool, err error)
   196  
   197  // snapWriteObserveFunc is called with the recovery system directory and the
   198  // path to a snap file being written. The snap file may be written to a location
   199  // under the common snaps directory.
   200  type snapWriteObserveFunc func(systemDir, where string) error
   201  
   202  // createSystemForModelFromValidatedSnaps creates a new recovery system for the
   203  // specified model with the specified label using the snaps in the database and
   204  // the getInfo function.
   205  //
   206  // The function returns the directory of the new recovery system as well as the
   207  // set of absolute file paths to the new snap files that were written for the
   208  // recovery system - some snaps may be in the recovery system directory while
   209  // others may be in the common snaps directory shared between multiple recovery
   210  // systems on ubuntu-seed.
   211  func createSystemForModelFromValidatedSnaps(model *asserts.Model, label string, db asserts.RODatabase, getInfo getSnapInfoFunc, observeWrite snapWriteObserveFunc) (dir string, err error) {
   212  	if model.Grade() == asserts.ModelGradeUnset {
   213  		return "", fmt.Errorf("cannot create a system for non UC20 model")
   214  	}
   215  
   216  	logger.Noticef("creating recovery system with label %q for %q", label, model.Model())
   217  
   218  	// TODO: should that path provided by boot package instead?
   219  	recoverySystemDirInRootDir := filepath.Join("/systems", label)
   220  	assertedSnapsDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps")
   221  	recoverySystemDir := filepath.Join(boot.InitramfsUbuntuSeedDir, recoverySystemDirInRootDir)
   222  
   223  	wOpts := &seedwriter.Options{
   224  		// RW mount of ubuntu-seed
   225  		SeedDir: boot.InitramfsUbuntuSeedDir,
   226  		Label:   label,
   227  	}
   228  	w, err := seedwriter.New(model, wOpts)
   229  	if err != nil {
   230  		return "", err
   231  	}
   232  
   233  	optsSnaps := make([]*seedwriter.OptionsSnap, 0, len(model.RequiredWithEssentialSnaps()))
   234  	// collect all snaps that are present
   235  	modelSnaps := make(map[string]*snap.Info)
   236  
   237  	getModelSnap := func(name string, essential bool, nonEssentialPresence string) error {
   238  		kind := "essential"
   239  		if !essential {
   240  			kind = "non-essential"
   241  			if nonEssentialPresence != "" {
   242  				kind = fmt.Sprintf("non-essential but %v", nonEssentialPresence)
   243  			}
   244  		}
   245  		info, present, err := getInfo(name)
   246  		if err != nil {
   247  			return fmt.Errorf("cannot obtain %v snap information: %v", kind, err)
   248  		}
   249  		if !essential && !present && nonEssentialPresence == "optional" {
   250  			// non-essential snap which is declared as optionally
   251  			// present in the model
   252  			return nil
   253  		}
   254  		// grab those
   255  		logger.Debugf("%v snap: %v", kind, name)
   256  		if !present {
   257  			return fmt.Errorf("internal error: %v snap %q not present", kind, name)
   258  		}
   259  		if _, ok := modelSnaps[info.MountFile()]; ok {
   260  			// we've already seen this snap
   261  			return nil
   262  		}
   263  		// present locally
   264  		// TODO: for grade dangerous we could have a channel here which is not
   265  		//       the model channel, handle that here
   266  		optsSnaps = append(optsSnaps, &seedwriter.OptionsSnap{
   267  			Path: info.MountFile(),
   268  		})
   269  		modelSnaps[info.MountFile()] = info
   270  		return nil
   271  	}
   272  
   273  	for _, sn := range model.EssentialSnaps() {
   274  		const essential = true
   275  		if err := getModelSnap(sn.SnapName(), essential, ""); err != nil {
   276  			return "", err
   277  		}
   278  	}
   279  	// snapd is implicitly needed
   280  	const snapdIsEssential = true
   281  	if err := getModelSnap("snapd", snapdIsEssential, ""); err != nil {
   282  		return "", err
   283  	}
   284  	for _, sn := range model.SnapsWithoutEssential() {
   285  		const essential = false
   286  		if err := getModelSnap(sn.SnapName(), essential, sn.Presence); err != nil {
   287  			return "", err
   288  		}
   289  	}
   290  	if err := w.SetOptionsSnaps(optsSnaps); err != nil {
   291  		return "", err
   292  	}
   293  
   294  	newFetcher := func(save func(asserts.Assertion) error) asserts.Fetcher {
   295  		fromDB := func(ref *asserts.Ref) (asserts.Assertion, error) {
   296  			return ref.Resolve(db.Find)
   297  		}
   298  		return asserts.NewFetcher(db, fromDB, save)
   299  	}
   300  	f, err := w.Start(db, newFetcher)
   301  	if err != nil {
   302  		return "", err
   303  	}
   304  	// past this point the system directory is present
   305  
   306  	localSnaps, err := w.LocalSnaps()
   307  	if err != nil {
   308  		return recoverySystemDir, err
   309  	}
   310  
   311  	for _, sn := range localSnaps {
   312  		info, ok := modelSnaps[sn.Path]
   313  		if !ok {
   314  			return recoverySystemDir, fmt.Errorf("internal error: no snap info for %q", sn.Path)
   315  		}
   316  		// TODO: the side info derived here can be different from what
   317  		// we have in snap.Info, but getting it this way can be
   318  		// expensive as we need to compute the hash, try to find a
   319  		// better way
   320  		_, aRefs, err := seedwriter.DeriveSideInfo(sn.Path, f, db)
   321  		if err != nil {
   322  			if !asserts.IsNotFound(err) {
   323  				return recoverySystemDir, err
   324  			} else if info.SnapID != "" {
   325  				// snap info from state must have come
   326  				// from the store, so it is unexpected
   327  				// if no assertions for it were found
   328  				return recoverySystemDir, fmt.Errorf("internal error: no assertions for asserted snap with ID: %v", info.SnapID)
   329  			}
   330  		}
   331  		if err := w.SetInfo(sn, info); err != nil {
   332  			return recoverySystemDir, err
   333  		}
   334  		sn.ARefs = aRefs
   335  	}
   336  
   337  	if err := w.InfoDerived(); err != nil {
   338  		return recoverySystemDir, err
   339  	}
   340  
   341  	for {
   342  		// get the list of snaps we need in this iteration
   343  		toDownload, err := w.SnapsToDownload()
   344  		if err != nil {
   345  			return recoverySystemDir, err
   346  		}
   347  		// which should be empty as all snaps should be accounted for
   348  		// already
   349  		if len(toDownload) > 0 {
   350  			which := make([]string, 0, len(toDownload))
   351  			for _, sn := range toDownload {
   352  				which = append(which, sn.SnapName())
   353  			}
   354  			return recoverySystemDir, fmt.Errorf("internal error: need to download snaps: %v", strings.Join(which, ", "))
   355  		}
   356  
   357  		complete, err := w.Downloaded()
   358  		if err != nil {
   359  			return recoverySystemDir, err
   360  		}
   361  		if complete {
   362  			logger.Debugf("snap processing for creating %q complete", label)
   363  			break
   364  		}
   365  	}
   366  
   367  	for _, warn := range w.Warnings() {
   368  		logger.Noticef("WARNING creating system %q: %s", label, warn)
   369  	}
   370  
   371  	unassertedSnaps, err := w.UnassertedSnaps()
   372  	if err != nil {
   373  		return recoverySystemDir, err
   374  	}
   375  	if len(unassertedSnaps) > 0 {
   376  		locals := make([]string, len(unassertedSnaps))
   377  		for i, sn := range unassertedSnaps {
   378  			locals[i] = sn.SnapName()
   379  		}
   380  		logger.Noticef("system %q contains unasserted snaps %s", label, strutil.Quoted(locals))
   381  	}
   382  
   383  	copySnap := func(name, src, dst string) error {
   384  		// if the destination snap is in the asserted snaps dir and already
   385  		// exists, we don't need to copy it since asserted snaps are shared
   386  		if strings.HasPrefix(dst, assertedSnapsDir+"/") && osutil.FileExists(dst) {
   387  			return nil
   388  		}
   389  		// otherwise, unasserted snaps are not shared, so even if the
   390  		// destination already exists if it is not in the asserted snaps we
   391  		// should copy it
   392  		logger.Noticef("copying new seed snap %q from %v to %v", name, src, dst)
   393  		if observeWrite != nil {
   394  			if err := observeWrite(recoverySystemDir, dst); err != nil {
   395  				return err
   396  			}
   397  		}
   398  		return osutil.CopyFile(src, dst, 0)
   399  	}
   400  	if err := w.SeedSnaps(copySnap); err != nil {
   401  		return recoverySystemDir, err
   402  	}
   403  	if err := w.WriteMeta(); err != nil {
   404  		return recoverySystemDir, err
   405  	}
   406  
   407  	bootSnaps, err := w.BootSnaps()
   408  	if err != nil {
   409  		return recoverySystemDir, err
   410  	}
   411  	bootWith := &boot.RecoverySystemBootableSet{}
   412  	for _, sn := range bootSnaps {
   413  		switch sn.Info.Type() {
   414  		case snap.TypeKernel:
   415  			bootWith.Kernel = sn.Info
   416  			bootWith.KernelPath = sn.Path
   417  		case snap.TypeGadget:
   418  			bootWith.GadgetSnapOrDir = sn.Path
   419  		}
   420  	}
   421  	if err := boot.MakeRecoverySystemBootable(boot.InitramfsUbuntuSeedDir, recoverySystemDirInRootDir, bootWith); err != nil {
   422  		return recoverySystemDir, fmt.Errorf("cannot make candidate recovery system %q bootable: %v", label, err)
   423  	}
   424  	logger.Noticef("created recovery system %q", label)
   425  
   426  	return recoverySystemDir, nil
   427  }