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