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

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-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 devicestate implements the manager and state aspects responsible
    21  // for the device identity and policies.
    22  package devicestate
    23  
    24  import (
    25  	"context"
    26  	"fmt"
    27  	"path/filepath"
    28  	"strconv"
    29  	"sync"
    30  
    31  	"github.com/snapcore/snapd/asserts"
    32  	"github.com/snapcore/snapd/boot"
    33  	"github.com/snapcore/snapd/gadget"
    34  	"github.com/snapcore/snapd/i18n"
    35  	"github.com/snapcore/snapd/logger"
    36  	"github.com/snapcore/snapd/netutil"
    37  	"github.com/snapcore/snapd/osutil"
    38  	"github.com/snapcore/snapd/overlord/assertstate"
    39  	"github.com/snapcore/snapd/overlord/auth"
    40  	"github.com/snapcore/snapd/overlord/configstate/config"
    41  	"github.com/snapcore/snapd/overlord/devicestate/internal"
    42  	"github.com/snapcore/snapd/overlord/ifacestate/ifacerepo"
    43  	"github.com/snapcore/snapd/overlord/snapstate"
    44  	"github.com/snapcore/snapd/overlord/state"
    45  	"github.com/snapcore/snapd/release"
    46  	"github.com/snapcore/snapd/snap"
    47  	"github.com/snapcore/snapd/snap/naming"
    48  )
    49  
    50  var (
    51  	snapstateInstallWithDeviceContext = snapstate.InstallWithDeviceContext
    52  	snapstateUpdateWithDeviceContext  = snapstate.UpdateWithDeviceContext
    53  )
    54  
    55  // findModel returns the device model assertion.
    56  func findModel(st *state.State) (*asserts.Model, error) {
    57  	device, err := internal.Device(st)
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  
    62  	if device.Brand == "" || device.Model == "" {
    63  		return nil, state.ErrNoState
    64  	}
    65  
    66  	a, err := assertstate.DB(st).Find(asserts.ModelType, map[string]string{
    67  		"series":   release.Series,
    68  		"brand-id": device.Brand,
    69  		"model":    device.Model,
    70  	})
    71  	if asserts.IsNotFound(err) {
    72  		return nil, state.ErrNoState
    73  	}
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	return a.(*asserts.Model), nil
    79  }
    80  
    81  // findSerial returns the device serial assertion. device is optional and used instead of the global state if provided.
    82  func findSerial(st *state.State, device *auth.DeviceState) (*asserts.Serial, error) {
    83  	if device == nil {
    84  		var err error
    85  		device, err = internal.Device(st)
    86  		if err != nil {
    87  			return nil, err
    88  		}
    89  	}
    90  
    91  	if device.Serial == "" {
    92  		return nil, state.ErrNoState
    93  	}
    94  
    95  	a, err := assertstate.DB(st).Find(asserts.SerialType, map[string]string{
    96  		"brand-id": device.Brand,
    97  		"model":    device.Model,
    98  		"serial":   device.Serial,
    99  	})
   100  	if asserts.IsNotFound(err) {
   101  		return nil, state.ErrNoState
   102  	}
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	return a.(*asserts.Serial), nil
   108  }
   109  
   110  // auto-refresh
   111  func canAutoRefresh(st *state.State) (bool, error) {
   112  	// we need to be seeded first
   113  	var seeded bool
   114  	st.Get("seeded", &seeded)
   115  	if !seeded {
   116  		return false, nil
   117  	}
   118  
   119  	// Either we have a serial or we try anyway if we attempted
   120  	// for a while to get a serial, this would allow us to at
   121  	// least upgrade core if that can help.
   122  	if ensureOperationalAttempts(st) >= 3 {
   123  		return true, nil
   124  	}
   125  
   126  	// Check model exists, for sanity. We always have a model, either
   127  	// seeded or a generic one that ships with snapd.
   128  	_, err := findModel(st)
   129  	if err == state.ErrNoState {
   130  		return false, nil
   131  	}
   132  	if err != nil {
   133  		return false, err
   134  	}
   135  
   136  	_, err = findSerial(st, nil)
   137  	if err == state.ErrNoState {
   138  		return false, nil
   139  	}
   140  	if err != nil {
   141  		return false, err
   142  	}
   143  
   144  	return true, nil
   145  }
   146  
   147  func checkGadgetOrKernel(st *state.State, snapInfo, curInfo *snap.Info, _ snap.Container, flags snapstate.Flags, deviceCtx snapstate.DeviceContext) error {
   148  	kind := ""
   149  	var snapType snap.Type
   150  	var getName func(*asserts.Model) string
   151  	switch snapInfo.Type() {
   152  	case snap.TypeGadget:
   153  		kind = "gadget"
   154  		snapType = snap.TypeGadget
   155  		getName = (*asserts.Model).Gadget
   156  	case snap.TypeKernel:
   157  		if release.OnClassic {
   158  			return fmt.Errorf("cannot install a kernel snap on classic")
   159  		}
   160  
   161  		kind = "kernel"
   162  		snapType = snap.TypeKernel
   163  		getName = (*asserts.Model).Kernel
   164  	default:
   165  		// not a relevant check
   166  		return nil
   167  	}
   168  
   169  	model := deviceCtx.Model()
   170  
   171  	if snapInfo.SnapID != "" {
   172  		snapDecl, err := assertstate.SnapDeclaration(st, snapInfo.SnapID)
   173  		if err != nil {
   174  			return fmt.Errorf("internal error: cannot find snap declaration for %q: %v", snapInfo.InstanceName(), err)
   175  		}
   176  		publisher := snapDecl.PublisherID()
   177  		if publisher != "canonical" && publisher != model.BrandID() {
   178  			return fmt.Errorf("cannot install %s %q published by %q for model by %q", kind, snapInfo.InstanceName(), publisher, model.BrandID())
   179  		}
   180  	} else {
   181  		logger.Noticef("installing unasserted %s %q", kind, snapInfo.InstanceName())
   182  	}
   183  
   184  	found, err := snapstate.HasSnapOfType(st, snapType)
   185  	if err != nil {
   186  		return fmt.Errorf("cannot detect original %s snap: %v", kind, err)
   187  	}
   188  	if found {
   189  		// already installed, snapstate takes care
   190  		return nil
   191  	}
   192  	// first installation of a gadget/kernel
   193  
   194  	expectedName := getName(model)
   195  	if expectedName == "" { // can happen only on classic
   196  		return fmt.Errorf("cannot install %s snap on classic if not requested by the model", kind)
   197  	}
   198  
   199  	if snapInfo.InstanceName() != snapInfo.SnapName() {
   200  		return fmt.Errorf("cannot install %q, parallel installation of kernel or gadget snaps is not supported", snapInfo.InstanceName())
   201  	}
   202  
   203  	if snapInfo.InstanceName() != expectedName {
   204  		return fmt.Errorf("cannot install %s %q, model assertion requests %q", kind, snapInfo.InstanceName(), expectedName)
   205  	}
   206  
   207  	return nil
   208  }
   209  
   210  func checkGadgetValid(st *state.State, snapInfo, _ *snap.Info, snapf snap.Container, flags snapstate.Flags, deviceCtx snapstate.DeviceContext) error {
   211  	if snapInfo.Type() != snap.TypeGadget {
   212  		// not a gadget, nothing to do
   213  		return nil
   214  	}
   215  	if deviceCtx.ForRemodeling() {
   216  		// in this case the gadget is checked by
   217  		// checkGadgetRemodelCompatible
   218  		return nil
   219  	}
   220  
   221  	// do basic validity checks on the gadget against its model constraints
   222  	_, err := gadget.ReadInfoFromSnapFile(snapf, deviceCtx.Model())
   223  	return err
   224  }
   225  
   226  var once sync.Once
   227  
   228  func delayedCrossMgrInit() {
   229  	once.Do(func() {
   230  		snapstate.AddCheckSnapCallback(checkGadgetOrKernel)
   231  		snapstate.AddCheckSnapCallback(checkGadgetValid)
   232  		snapstate.AddCheckSnapCallback(checkGadgetRemodelCompatible)
   233  	})
   234  	snapstate.CanAutoRefresh = canAutoRefresh
   235  	snapstate.CanManageRefreshes = CanManageRefreshes
   236  	snapstate.IsOnMeteredConnection = netutil.IsOnMeteredConnection
   237  	snapstate.DeviceCtx = DeviceCtx
   238  	snapstate.Remodeling = Remodeling
   239  }
   240  
   241  // proxyStore returns the store assertion for the proxy store if one is set.
   242  func proxyStore(st *state.State, tr *config.Transaction) (*asserts.Store, error) {
   243  	var proxyStore string
   244  	err := tr.GetMaybe("core", "proxy.store", &proxyStore)
   245  	if err != nil {
   246  		return nil, err
   247  	}
   248  	if proxyStore == "" {
   249  		return nil, state.ErrNoState
   250  	}
   251  
   252  	a, err := assertstate.DB(st).Find(asserts.StoreType, map[string]string{
   253  		"store": proxyStore,
   254  	})
   255  	if asserts.IsNotFound(err) {
   256  		return nil, state.ErrNoState
   257  	}
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  
   262  	return a.(*asserts.Store), nil
   263  }
   264  
   265  // interfaceConnected returns true if the given snap/interface names
   266  // are connected
   267  func interfaceConnected(st *state.State, snapName, ifName string) bool {
   268  	conns, err := ifacerepo.Get(st).Connected(snapName, ifName)
   269  	return err == nil && len(conns) > 0
   270  }
   271  
   272  // CanManageRefreshes returns true if the device can be
   273  // switched to the "core.refresh.schedule=managed" mode.
   274  //
   275  // TODO:
   276  // - Move the CanManageRefreshes code into the ifstate
   277  // - Look at the connections and find the connection for snapd-control
   278  //   with the managed attribute
   279  // - Take the snap from this connection and look at the snapstate to see
   280  //   if that snap has a snap declaration (to ensure it comes from the store)
   281  func CanManageRefreshes(st *state.State) bool {
   282  	snapStates, err := snapstate.All(st)
   283  	if err != nil {
   284  		return false
   285  	}
   286  	for _, snapst := range snapStates {
   287  		// Always get the current info even if the snap is currently
   288  		// being operated on or if its disabled.
   289  		info, err := snapst.CurrentInfo()
   290  		if err != nil {
   291  			continue
   292  		}
   293  		if info.Broken != "" {
   294  			continue
   295  		}
   296  		// The snap must have a snap declaration (implies that
   297  		// its from the store)
   298  		if _, err := assertstate.SnapDeclaration(st, info.SideInfo.SnapID); err != nil {
   299  			continue
   300  		}
   301  
   302  		for _, plugInfo := range info.Plugs {
   303  			if plugInfo.Interface == "snapd-control" && plugInfo.Attrs["refresh-schedule"] == "managed" {
   304  				snapName := info.InstanceName()
   305  				plugName := plugInfo.Name
   306  				if interfaceConnected(st, snapName, plugName) {
   307  					return true
   308  				}
   309  			}
   310  		}
   311  	}
   312  
   313  	return false
   314  }
   315  
   316  func getAllRequiredSnapsForModel(model *asserts.Model) *naming.SnapSet {
   317  	reqSnaps := model.RequiredWithEssentialSnaps()
   318  	return naming.NewSnapSet(reqSnaps)
   319  }
   320  
   321  // extractDownloadInstallEdgesFromTs extracts the first, last download
   322  // phase and install phase tasks from a TaskSet
   323  func extractDownloadInstallEdgesFromTs(ts *state.TaskSet) (firstDl, lastDl, firstInst, lastInst *state.Task, err error) {
   324  	edgeTask, err := ts.Edge(snapstate.DownloadAndChecksDoneEdge)
   325  	if err != nil {
   326  		return nil, nil, nil, nil, err
   327  	}
   328  	tasks := ts.Tasks()
   329  	// we know we always start with downloads
   330  	firstDl = tasks[0]
   331  	// and always end with installs
   332  	lastInst = tasks[len(tasks)-1]
   333  
   334  	var edgeTaskIndex int
   335  	for i, task := range tasks {
   336  		if task == edgeTask {
   337  			edgeTaskIndex = i
   338  			break
   339  		}
   340  	}
   341  	return firstDl, tasks[edgeTaskIndex], tasks[edgeTaskIndex+1], lastInst, nil
   342  }
   343  
   344  func notInstalled(st *state.State, name string) (bool, error) {
   345  	_, err := snapstate.CurrentInfo(st, name)
   346  	_, isNotInstalled := err.(*snap.NotInstalledError)
   347  	if isNotInstalled {
   348  		return true, nil
   349  	}
   350  	return false, err
   351  }
   352  
   353  func remodelTasks(ctx context.Context, st *state.State, current, new *asserts.Model, deviceCtx snapstate.DeviceContext, fromChange string) ([]*state.TaskSet, error) {
   354  	userID := 0
   355  	var tss []*state.TaskSet
   356  
   357  	// kernel
   358  	if current.Kernel() == new.Kernel() && current.KernelTrack() != new.KernelTrack() {
   359  		ts, err := snapstateUpdateWithDeviceContext(st, new.Kernel(), &snapstate.RevisionOptions{Channel: new.KernelTrack()}, userID, snapstate.Flags{NoReRefresh: true}, deviceCtx, fromChange)
   360  		if err != nil {
   361  			return nil, err
   362  		}
   363  		tss = append(tss, ts)
   364  	}
   365  
   366  	var ts *state.TaskSet
   367  	if current.Kernel() != new.Kernel() {
   368  		needsInstall, err := notInstalled(st, new.Kernel())
   369  		if err != nil {
   370  			return nil, err
   371  		}
   372  		if needsInstall {
   373  			ts, err = snapstateInstallWithDeviceContext(ctx, st, new.Kernel(), &snapstate.RevisionOptions{Channel: new.KernelTrack()}, userID, snapstate.Flags{}, deviceCtx, fromChange)
   374  		} else {
   375  			ts, err = snapstate.LinkNewBaseOrKernel(st, new.Base())
   376  		}
   377  		if err != nil {
   378  			return nil, err
   379  		}
   380  		tss = append(tss, ts)
   381  	}
   382  	if current.Base() != new.Base() {
   383  		needsInstall, err := notInstalled(st, new.Base())
   384  		if err != nil {
   385  			return nil, err
   386  		}
   387  		if needsInstall {
   388  			ts, err = snapstateInstallWithDeviceContext(ctx, st, new.Base(), nil, userID, snapstate.Flags{}, deviceCtx, fromChange)
   389  		} else {
   390  			ts, err = snapstate.LinkNewBaseOrKernel(st, new.Base())
   391  		}
   392  		if err != nil {
   393  			return nil, err
   394  		}
   395  		tss = append(tss, ts)
   396  	}
   397  	// gadget
   398  	if current.Gadget() == new.Gadget() && current.GadgetTrack() != new.GadgetTrack() {
   399  		ts, err := snapstateUpdateWithDeviceContext(st, new.Gadget(), &snapstate.RevisionOptions{Channel: new.GadgetTrack()}, userID, snapstate.Flags{NoReRefresh: true}, deviceCtx, fromChange)
   400  		if err != nil {
   401  			return nil, err
   402  		}
   403  		tss = append(tss, ts)
   404  	}
   405  	if current.Gadget() != new.Gadget() {
   406  		ts, err := snapstateInstallWithDeviceContext(ctx, st, new.Gadget(), &snapstate.RevisionOptions{Channel: new.GadgetTrack()}, userID, snapstate.Flags{}, deviceCtx, fromChange)
   407  		if err != nil {
   408  			return nil, err
   409  		}
   410  		tss = append(tss, ts)
   411  	}
   412  
   413  	// add new required-snaps, no longer required snaps will be cleaned
   414  	// in "set-model"
   415  	for _, snapRef := range new.RequiredNoEssentialSnaps() {
   416  		// TODO|XXX: have methods that take refs directly
   417  		// to respect the snap ids
   418  		needsInstall, err := notInstalled(st, snapRef.SnapName())
   419  		if err != nil {
   420  			return nil, err
   421  		}
   422  		if needsInstall {
   423  			// If the snap is not installed we need to install it now.
   424  			ts, err := snapstateInstallWithDeviceContext(ctx, st, snapRef.SnapName(), nil, userID, snapstate.Flags{Required: true}, deviceCtx, fromChange)
   425  			if err != nil {
   426  				return nil, err
   427  			}
   428  			tss = append(tss, ts)
   429  		}
   430  	}
   431  	// TODO: Validate that all bases and default-providers are part
   432  	//       of the install tasksets and error if not. If the
   433  	//       prereq task handler check starts adding installs into
   434  	//       our remodel change our carefully constructed wait chain
   435  	//       breaks down.
   436  
   437  	// Keep track of downloads tasks carrying snap-setup which is needed for
   438  	// recovery system tasks
   439  	var snapSetupTasks []string
   440  
   441  	// Ensure all download/check tasks are run *before* the install
   442  	// tasks. During a remodel the network may not be available so
   443  	// we need to ensure we have everything local.
   444  	var lastDownloadInChain, firstInstallInChain *state.Task
   445  	var prevDownload, prevInstall *state.Task
   446  	for _, ts := range tss {
   447  		// make sure all things happen sequentially
   448  		// Terminology
   449  		// A <- B means B waits for A
   450  		// "download,verify" are part of the "Download" phase
   451  		// "link,start" is part of "Install" phase
   452  		//
   453  		// - all tasks inside ts{Download,Install} already wait for
   454  		//   each other so the chains look something like this:
   455  		//     download1 <- verify1 <- install1
   456  		//     download2 <- verify2 <- install2
   457  		//     download3 <- verify3 <- install3
   458  		// - add wait of each first ts{Download,Install} task for
   459  		//   the last previous ts{Download,Install} task
   460  		//   Our chains now looks like:
   461  		//     download1 <- verify1 <- install1 (as before)
   462  		//     download2 <- verify2 <- install2 (as before)
   463  		//     download3 <- verify3 <- install3 (as before)
   464  		//     verify1 <- download2 (added)
   465  		//     verify2 <- download3 (added)
   466  		//     install1  <- install2 (added)
   467  		//     install2  <- install3 (added)
   468  		downloadStart, downloadLast, installFirst, installLast, err := extractDownloadInstallEdgesFromTs(ts)
   469  		if err != nil {
   470  			return nil, fmt.Errorf("cannot remodel: %v", err)
   471  		}
   472  		if prevDownload != nil {
   473  			// XXX: we don't strictly need to serialize the download
   474  			downloadStart.WaitFor(prevDownload)
   475  		}
   476  		if prevInstall != nil {
   477  			installFirst.WaitFor(prevInstall)
   478  		}
   479  		prevDownload = downloadLast
   480  		prevInstall = installLast
   481  		// update global state
   482  		lastDownloadInChain = downloadLast
   483  		if firstInstallInChain == nil {
   484  			firstInstallInChain = installFirst
   485  		}
   486  		// download is always a first task of the 'download' phase
   487  		snapSetupTasks = append(snapSetupTasks, downloadStart.ID())
   488  	}
   489  	// Make sure the first install waits for the recovery system (only in
   490  	// UC20) which waits for the last download. With this our (simplified)
   491  	// wait chain looks like this:
   492  	//
   493  	// download1
   494  	//   ^- verify1
   495  	//        ^- download2
   496  	//             ^- verify2
   497  	//                  ^- download3
   498  	//                       ^- verify3
   499  	//                            ^- recovery (UC20)
   500  	//                                 ^- install1
   501  	//                                      ^- install2
   502  	//                                           ^- install3
   503  	if firstInstallInChain != nil && lastDownloadInChain != nil {
   504  		firstInstallInChain.WaitFor(lastDownloadInChain)
   505  	}
   506  
   507  	recoverySetupTaskID := ""
   508  	if new.Grade() != asserts.ModelGradeUnset {
   509  		// create a recovery when remodeling to a UC20 system, actual
   510  		// policy for possible remodels has already been verified by the
   511  		// caller
   512  		labelBase := timeNow().Format("20060102")
   513  		label, err := pickRecoverySystemLabel(labelBase)
   514  		if err != nil {
   515  			return nil, fmt.Errorf("cannot select non-conflicting label for recovery system %q: %v", labelBase, err)
   516  		}
   517  		createRecoveryTasks, err := createRecoverySystemTasks(st, label, snapSetupTasks)
   518  		if err != nil {
   519  			return nil, err
   520  		}
   521  		if lastDownloadInChain != nil {
   522  			// wait for all snaps that need to be downloaded
   523  			createRecoveryTasks.WaitFor(lastDownloadInChain)
   524  		}
   525  		if firstInstallInChain != nil {
   526  			// when any snap installations need to happen, they
   527  			// should also wait for recovery system to be created
   528  			firstInstallInChain.WaitAll(createRecoveryTasks)
   529  		}
   530  		tss = append(tss, createRecoveryTasks)
   531  		recoverySetupTaskID = createRecoveryTasks.Tasks()[0].ID()
   532  	}
   533  
   534  	// Set the new model assertion - this *must* be the last thing done
   535  	// by the change.
   536  	setModel := st.NewTask("set-model", i18n.G("Set new model assertion"))
   537  	for _, tsPrev := range tss {
   538  		setModel.WaitAll(tsPrev)
   539  	}
   540  	if recoverySetupTaskID != "" {
   541  		// set model needs to access information about the recovery
   542  		// system
   543  		setModel.Set("recovery-system-setup-task", recoverySetupTaskID)
   544  	}
   545  	tss = append(tss, state.NewTaskSet(setModel))
   546  
   547  	return tss, nil
   548  }
   549  
   550  var allowUC20RemodelTesting = false
   551  
   552  // AllowUC20RemodelTesting is a temporary helper to allow testing remodeling of
   553  // UC20 before the implementation is complete and the policy for this settled.
   554  // It will be removed once remodel is fully implemented and made available for
   555  // general use.
   556  func AllowUC20RemodelTesting(allow bool) (restore func()) {
   557  	osutil.MustBeTestBinary("uc20 remodel testing only can be mocked in tests")
   558  	oldAllowUC20RemodelTesting := allowUC20RemodelTesting
   559  	allowUC20RemodelTesting = allow
   560  	return func() {
   561  		allowUC20RemodelTesting = oldAllowUC20RemodelTesting
   562  	}
   563  }
   564  
   565  // Remodel takes a new model assertion and generates a change that
   566  // takes the device from the old to the new model or an error if the
   567  // transition is not possible.
   568  //
   569  // TODO:
   570  // - Check estimated disk size delta
   571  // - Check all relevant snaps exist in new store
   572  //   (need to check that even unchanged snaps are accessible)
   573  // - Make sure this works with Core 20 as well, in the Core 20 case
   574  //   we must enforce the default-channels from the model as well
   575  func Remodel(st *state.State, new *asserts.Model) (*state.Change, error) {
   576  	var seeded bool
   577  	err := st.Get("seeded", &seeded)
   578  	if err != nil && err != state.ErrNoState {
   579  		return nil, err
   580  	}
   581  	if !seeded {
   582  		return nil, fmt.Errorf("cannot remodel until fully seeded")
   583  	}
   584  
   585  	current, err := findModel(st)
   586  	if err != nil {
   587  		return nil, err
   588  	}
   589  
   590  	if _, err := findSerial(st, nil); err != nil {
   591  		if err == state.ErrNoState {
   592  			return nil, fmt.Errorf("cannot remodel without a serial")
   593  		}
   594  		return nil, err
   595  	}
   596  
   597  	if current.Series() != new.Series() {
   598  		return nil, fmt.Errorf("cannot remodel to different series yet")
   599  	}
   600  
   601  	// TODO:UC20: support remodel, also ensure we never remodel to a lower
   602  	// grade
   603  	if !allowUC20RemodelTesting {
   604  		if current.Grade() != asserts.ModelGradeUnset {
   605  			return nil, fmt.Errorf("cannot remodel Ubuntu Core 20 models yet")
   606  		}
   607  		if new.Grade() != asserts.ModelGradeUnset {
   608  			return nil, fmt.Errorf("cannot remodel to Ubuntu Core 20 models yet")
   609  		}
   610  	} else {
   611  		// also disallows remodel from non-UC20 (grade unset) to UC20
   612  		if current.Grade() != new.Grade() {
   613  			return nil, fmt.Errorf("cannot remodel from grade %v to grade %v", current.Grade(), new.Grade())
   614  		}
   615  	}
   616  
   617  	// TODO: we need dedicated assertion language to permit for
   618  	// model transitions before we allow cross vault
   619  	// transitions.
   620  
   621  	remodelKind := ClassifyRemodel(current, new)
   622  
   623  	// TODO: should we restrict remodel from one arch to another?
   624  	// There are valid use-cases here though, i.e. amd64 machine that
   625  	// remodels itself to/from i386 (if the HW can do both 32/64 bit)
   626  	if current.Architecture() != new.Architecture() {
   627  		return nil, fmt.Errorf("cannot remodel to different architectures yet")
   628  	}
   629  
   630  	// calculate snap differences between the two models
   631  	// FIXME: this needs work to switch from core->bases
   632  	if current.Base() == "" && new.Base() != "" {
   633  		return nil, fmt.Errorf("cannot remodel from core to bases yet")
   634  	}
   635  
   636  	// Do we do this only for the more complicated cases (anything
   637  	// more than adding required-snaps really)?
   638  	if err := snapstate.CheckChangeConflictRunExclusively(st, "remodel"); err != nil {
   639  		return nil, err
   640  	}
   641  
   642  	remodCtx, err := remodelCtx(st, current, new)
   643  	if err != nil {
   644  		return nil, err
   645  	}
   646  
   647  	var tss []*state.TaskSet
   648  	switch remodelKind {
   649  	case ReregRemodel:
   650  		requestSerial := st.NewTask("request-serial", i18n.G("Request new device serial"))
   651  
   652  		prepare := st.NewTask("prepare-remodeling", i18n.G("Prepare remodeling"))
   653  		prepare.WaitFor(requestSerial)
   654  		ts := state.NewTaskSet(requestSerial, prepare)
   655  		tss = []*state.TaskSet{ts}
   656  	case StoreSwitchRemodel:
   657  		sto := remodCtx.Store()
   658  		if sto == nil {
   659  			return nil, fmt.Errorf("internal error: a store switch remodeling should have built a store")
   660  		}
   661  		// ensure a new session accounting for the new brand store
   662  		st.Unlock()
   663  		_, err := sto.EnsureDeviceSession()
   664  		st.Lock()
   665  		if err != nil {
   666  			return nil, fmt.Errorf("cannot get a store session based on the new model assertion: %v", err)
   667  		}
   668  		fallthrough
   669  	case UpdateRemodel:
   670  		var err error
   671  		tss, err = remodelTasks(context.TODO(), st, current, new, remodCtx, "")
   672  		if err != nil {
   673  			return nil, err
   674  		}
   675  	}
   676  
   677  	// we potentially released the lock a couple of times here:
   678  	// make sure the current model is essentially the same as when
   679  	// we started
   680  	current1, err := findModel(st)
   681  	if err != nil {
   682  		return nil, err
   683  	}
   684  	if current.BrandID() != current1.BrandID() || current.Model() != current1.Model() || current.Revision() != current1.Revision() {
   685  		return nil, &snapstate.ChangeConflictError{Message: fmt.Sprintf("cannot start remodel, clashing with concurrent remodel to %v/%v (%v)", current1.BrandID(), current1.Model(), current1.Revision())}
   686  	}
   687  	// make sure another unfinished remodel wasn't already setup either
   688  	if Remodeling(st) {
   689  		return nil, &snapstate.ChangeConflictError{Message: "cannot start remodel, clashing with concurrent one"}
   690  	}
   691  
   692  	var msg string
   693  	if current.BrandID() == new.BrandID() && current.Model() == new.Model() {
   694  		msg = fmt.Sprintf(i18n.G("Refresh model assertion from revision %v to %v"), current.Revision(), new.Revision())
   695  	} else {
   696  		msg = fmt.Sprintf(i18n.G("Remodel device to %v/%v (%v)"), new.BrandID(), new.Model(), new.Revision())
   697  	}
   698  
   699  	chg := st.NewChange("remodel", msg)
   700  	remodCtx.Init(chg)
   701  	for _, ts := range tss {
   702  		chg.AddAll(ts)
   703  	}
   704  
   705  	return chg, nil
   706  }
   707  
   708  // Remodeling returns true whether there's a remodeling in progress
   709  func Remodeling(st *state.State) bool {
   710  	for _, chg := range st.Changes() {
   711  		if !chg.IsReady() && chg.Kind() == "remodel" {
   712  			return true
   713  		}
   714  	}
   715  	return false
   716  }
   717  
   718  type recoverySystemSetup struct {
   719  	// Label of the recovery system, selected when tasks are created
   720  	Label string `json:"label"`
   721  	// Directory inside the seed filesystem where the recovery system files
   722  	// are kept, typically /run/mnt/ubuntu-seed/systems/<label>, set when
   723  	// tasks are created
   724  	Directory string `json:"directory"`
   725  	// SnapSetupTasks is a list of task IDs that carry snap setup
   726  	// information, relevant only during remodel, set when tasks are created
   727  	SnapSetupTasks []string `json:"snap-setup-tasks"`
   728  }
   729  
   730  func pickRecoverySystemLabel(labelBase string) (string, error) {
   731  	systemDirectory := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", labelBase)
   732  	exists, _, err := osutil.DirExists(systemDirectory)
   733  	if err != nil {
   734  		return "", err
   735  	}
   736  	if !exists {
   737  		return labelBase, nil
   738  	}
   739  	// pick alternative, which is named like <label>-<number>
   740  	present, err := filepath.Glob(systemDirectory + "-*")
   741  	if err != nil {
   742  		return "", err
   743  	}
   744  	maxExistingNumber := 0
   745  	for _, existingDir := range present {
   746  		suffix := existingDir[len(systemDirectory)+1:]
   747  		num, err := strconv.Atoi(suffix)
   748  		if err != nil {
   749  			// non numerical suffix?
   750  			continue
   751  		}
   752  		if num > maxExistingNumber {
   753  			maxExistingNumber = num
   754  		}
   755  	}
   756  	return fmt.Sprintf("%s-%d", labelBase, maxExistingNumber+1), nil
   757  }
   758  
   759  func createRecoverySystemTasks(st *state.State, label string, snapSetupTasks []string) (*state.TaskSet, error) {
   760  	// sanity check, the directory should not exist yet
   761  	systemDirectory := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", label)
   762  	exists, _, err := osutil.DirExists(systemDirectory)
   763  	if err != nil {
   764  		return nil, err
   765  	}
   766  	if exists {
   767  		return nil, fmt.Errorf("recovery system %q already exists", label)
   768  	}
   769  
   770  	create := st.NewTask("create-recovery-system", fmt.Sprintf("Create recovery system with label %q", label))
   771  	// the label we want
   772  	create.Set("recovery-system-setup", &recoverySystemSetup{
   773  		Label:     label,
   774  		Directory: systemDirectory,
   775  		// IDs of the tasks carrying snap-setup
   776  		SnapSetupTasks: snapSetupTasks,
   777  	})
   778  
   779  	finalize := st.NewTask("finalize-recovery-system", fmt.Sprintf("Finalize recovery system with label %q", label))
   780  	finalize.WaitFor(create)
   781  	// finalize needs to know the label too
   782  	finalize.Set("recovery-system-setup-task", create.ID())
   783  	return state.NewTaskSet(create, finalize), nil
   784  }
   785  
   786  func CreateRecoverySystem(st *state.State, label string) (*state.Change, error) {
   787  	var seeded bool
   788  	err := st.Get("seeded", &seeded)
   789  	if err != nil && err != state.ErrNoState {
   790  		return nil, err
   791  	}
   792  	if !seeded {
   793  		return nil, fmt.Errorf("cannot create new recovery systems until fully seeded")
   794  	}
   795  	chg := st.NewChange("create-recovery-system", fmt.Sprintf("Create new recovery system with label %q", label))
   796  	ts, err := createRecoverySystemTasks(st, label, nil)
   797  	if err != nil {
   798  		return nil, err
   799  	}
   800  	chg.AddAll(ts)
   801  	return chg, nil
   802  }