github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/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  	return &whatseeded[0], nil
   160  }
   161  
   162  func seededSystemFromModeenv() (*seededSystem, error) {
   163  	modeEnv, err := maybeReadModeenv()
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  	if modeEnv == nil {
   168  		return nil, fmt.Errorf("internal error: modeenv does not exist")
   169  	}
   170  	if modeEnv.RecoverySystem == "" {
   171  		return nil, fmt.Errorf("internal error: recovery system is unset")
   172  	}
   173  
   174  	system, err := systemFromSeed(modeEnv.RecoverySystem, nil)
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  	seededSys := &seededSystem{
   179  		System:    modeEnv.RecoverySystem,
   180  		Model:     system.Model.Model(),
   181  		BrandID:   system.Model.BrandID(),
   182  		Revision:  system.Model.Revision(),
   183  		Timestamp: system.Model.Timestamp(),
   184  		// SeedTime is intentionally left unset
   185  	}
   186  	return seededSys, nil
   187  }
   188  
   189  // getInfoFunc is expected to return for a given snap name a snap.Info for that
   190  // snap and whether the snap is present is present. The second bit is relevant
   191  // for non-essential snaps mentioned in the model, which if present and having
   192  // an 'optional' presence in the model, will be added to the recovery system.
   193  type getSnapInfoFunc func(name string) (info *snap.Info, snapIsPresent bool, err error)
   194  
   195  // snapWriteObserveFunc is called with the recovery system directory and the
   196  // path to a snap file being written. The snap file may be written to a location
   197  // under the common snaps directory.
   198  type snapWriteObserveFunc func(systemDir, where string) error
   199  
   200  // createSystemForModelFromValidatedSnaps creates a new recovery system for the
   201  // specified model with the specified label using the snaps in the database and
   202  // the getInfo function.
   203  //
   204  // The function returns the directory of the new recovery system as well as the
   205  // set of absolute file paths to the new snap files that were written for the
   206  // recovery system - some snaps may be in the recovery system directory while
   207  // others may be in the common snaps directory shared between multiple recovery
   208  // systems on ubuntu-seed.
   209  func createSystemForModelFromValidatedSnaps(model *asserts.Model, label string, db asserts.RODatabase, getInfo getSnapInfoFunc, observeWrite snapWriteObserveFunc) (dir string, err error) {
   210  	if model.Grade() == asserts.ModelGradeUnset {
   211  		return "", fmt.Errorf("cannot create a system for non UC20 model")
   212  	}
   213  
   214  	logger.Noticef("creating recovery system with label %q for %q", label, model.Model())
   215  
   216  	// TODO: should that path provided by boot package instead?
   217  	recoverySystemDirInRootDir := filepath.Join("/systems", label)
   218  	assertedSnapsDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps")
   219  	recoverySystemDir := filepath.Join(boot.InitramfsUbuntuSeedDir, recoverySystemDirInRootDir)
   220  
   221  	wOpts := &seedwriter.Options{
   222  		// RW mount of ubuntu-seed
   223  		SeedDir: boot.InitramfsUbuntuSeedDir,
   224  		Label:   label,
   225  	}
   226  	w, err := seedwriter.New(model, wOpts)
   227  	if err != nil {
   228  		return "", err
   229  	}
   230  
   231  	optsSnaps := make([]*seedwriter.OptionsSnap, 0, len(model.RequiredWithEssentialSnaps()))
   232  	// collect all snaps that are present
   233  	modelSnaps := make(map[string]*snap.Info)
   234  
   235  	getModelSnap := func(name string, essential bool, nonEssentialPresence string) error {
   236  		kind := "essential"
   237  		if !essential {
   238  			kind = "non-essential"
   239  			if nonEssentialPresence != "" {
   240  				kind = fmt.Sprintf("non-essential but %v", nonEssentialPresence)
   241  			}
   242  		}
   243  		info, present, err := getInfo(name)
   244  		if err != nil {
   245  			return fmt.Errorf("cannot obtain %v snap information: %v", kind, err)
   246  		}
   247  		if !essential && !present && nonEssentialPresence == "optional" {
   248  			// non-essential snap which is declared as optionally
   249  			// present in the model
   250  			return nil
   251  		}
   252  		// grab those
   253  		logger.Debugf("%v snap: %v", kind, name)
   254  		if !present {
   255  			return fmt.Errorf("internal error: %v snap %q not present", kind, name)
   256  		}
   257  		if _, ok := modelSnaps[info.MountFile()]; ok {
   258  			// we've already seen this snap
   259  			return nil
   260  		}
   261  		// present locally
   262  		// TODO: for grade dangerous we could have a channel here which is not
   263  		//       the model channel, handle that here
   264  		optsSnaps = append(optsSnaps, &seedwriter.OptionsSnap{
   265  			Path: info.MountFile(),
   266  		})
   267  		modelSnaps[info.MountFile()] = info
   268  		return nil
   269  	}
   270  
   271  	for _, sn := range model.EssentialSnaps() {
   272  		const essential = true
   273  		if err := getModelSnap(sn.SnapName(), essential, ""); err != nil {
   274  			return "", err
   275  		}
   276  	}
   277  	// snapd is implicitly needed
   278  	const snapdIsEssential = true
   279  	if err := getModelSnap("snapd", snapdIsEssential, ""); err != nil {
   280  		return "", err
   281  	}
   282  	for _, sn := range model.SnapsWithoutEssential() {
   283  		const essential = false
   284  		if err := getModelSnap(sn.SnapName(), essential, sn.Presence); err != nil {
   285  			return "", err
   286  		}
   287  	}
   288  	if err := w.SetOptionsSnaps(optsSnaps); err != nil {
   289  		return "", err
   290  	}
   291  
   292  	newFetcher := func(save func(asserts.Assertion) error) asserts.Fetcher {
   293  		fromDB := func(ref *asserts.Ref) (asserts.Assertion, error) {
   294  			return ref.Resolve(db.Find)
   295  		}
   296  		return asserts.NewFetcher(db, fromDB, save)
   297  	}
   298  	f, err := w.Start(db, newFetcher)
   299  	if err != nil {
   300  		return "", err
   301  	}
   302  	// past this point the system directory is present
   303  
   304  	localSnaps, err := w.LocalSnaps()
   305  	if err != nil {
   306  		return recoverySystemDir, err
   307  	}
   308  
   309  	for _, sn := range localSnaps {
   310  		info, ok := modelSnaps[sn.Path]
   311  		if !ok {
   312  			return recoverySystemDir, fmt.Errorf("internal error: no snap info for %q", sn.Path)
   313  		}
   314  		// TODO: the side info derived here can be different from what
   315  		// we have in snap.Info, but getting it this way can be
   316  		// expensive as we need to compute the hash, try to find a
   317  		// better way
   318  		_, aRefs, err := seedwriter.DeriveSideInfo(sn.Path, f, db)
   319  		if err != nil {
   320  			if !asserts.IsNotFound(err) {
   321  				return recoverySystemDir, err
   322  			} else if info.SnapID != "" {
   323  				// snap info from state must have come
   324  				// from the store, so it is unexpected
   325  				// if no assertions for it were found
   326  				return recoverySystemDir, fmt.Errorf("internal error: no assertions for asserted snap with ID: %v", info.SnapID)
   327  			}
   328  		}
   329  		if err := w.SetInfo(sn, info); err != nil {
   330  			return recoverySystemDir, err
   331  		}
   332  		sn.ARefs = aRefs
   333  	}
   334  
   335  	if err := w.InfoDerived(); err != nil {
   336  		return recoverySystemDir, err
   337  	}
   338  
   339  	for {
   340  		// get the list of snaps we need in this iteration
   341  		toDownload, err := w.SnapsToDownload()
   342  		if err != nil {
   343  			return recoverySystemDir, err
   344  		}
   345  		// which should be empty as all snaps should be accounted for
   346  		// already
   347  		if len(toDownload) > 0 {
   348  			which := make([]string, 0, len(toDownload))
   349  			for _, sn := range toDownload {
   350  				which = append(which, sn.SnapName())
   351  			}
   352  			return recoverySystemDir, fmt.Errorf("internal error: need to download snaps: %v", strings.Join(which, ", "))
   353  		}
   354  
   355  		complete, err := w.Downloaded()
   356  		if err != nil {
   357  			return recoverySystemDir, err
   358  		}
   359  		if complete {
   360  			logger.Debugf("snap processing for creating %q complete", label)
   361  			break
   362  		}
   363  	}
   364  
   365  	for _, warn := range w.Warnings() {
   366  		logger.Noticef("WARNING creating system %q: %s", label, warn)
   367  	}
   368  
   369  	unassertedSnaps, err := w.UnassertedSnaps()
   370  	if err != nil {
   371  		return recoverySystemDir, err
   372  	}
   373  	if len(unassertedSnaps) > 0 {
   374  		locals := make([]string, len(unassertedSnaps))
   375  		for i, sn := range unassertedSnaps {
   376  			locals[i] = sn.SnapName()
   377  		}
   378  		logger.Noticef("system %q contains unasserted snaps %s", label, strutil.Quoted(locals))
   379  	}
   380  
   381  	copySnap := func(name, src, dst string) error {
   382  		// if the destination snap is in the asserted snaps dir and already
   383  		// exists, we don't need to copy it since asserted snaps are shared
   384  		if strings.HasPrefix(dst, assertedSnapsDir+"/") && osutil.FileExists(dst) {
   385  			return nil
   386  		}
   387  		// otherwise, unasserted snaps are not shared, so even if the
   388  		// destination already exists if it is not in the asserted snaps we
   389  		// should copy it
   390  		logger.Noticef("copying new seed snap %q from %v to %v", name, src, dst)
   391  		if observeWrite != nil {
   392  			if err := observeWrite(recoverySystemDir, dst); err != nil {
   393  				return err
   394  			}
   395  		}
   396  		return osutil.CopyFile(src, dst, 0)
   397  	}
   398  	if err := w.SeedSnaps(copySnap); err != nil {
   399  		return recoverySystemDir, err
   400  	}
   401  	if err := w.WriteMeta(); err != nil {
   402  		return recoverySystemDir, err
   403  	}
   404  
   405  	bootSnaps, err := w.BootSnaps()
   406  	if err != nil {
   407  		return recoverySystemDir, err
   408  	}
   409  	bootWith := &boot.RecoverySystemBootableSet{}
   410  	for _, sn := range bootSnaps {
   411  		switch sn.Info.Type() {
   412  		case snap.TypeKernel:
   413  			bootWith.Kernel = sn.Info
   414  			bootWith.KernelPath = sn.Path
   415  		case snap.TypeGadget:
   416  			bootWith.GadgetSnapOrDir = sn.Path
   417  		}
   418  	}
   419  	if err := boot.MakeRecoverySystemBootable(boot.InitramfsUbuntuSeedDir, recoverySystemDirInRootDir, bootWith); err != nil {
   420  		return recoverySystemDir, fmt.Errorf("cannot make candidate recovery system %q bootable: %v", label, err)
   421  	}
   422  	logger.Noticef("created recovery system %q", label)
   423  
   424  	return recoverySystemDir, nil
   425  }