github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/overlord/devicestate/remodel.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package devicestate
    21  
    22  import (
    23  	"fmt"
    24  	"time"
    25  
    26  	"github.com/snapcore/snapd/asserts"
    27  	"github.com/snapcore/snapd/boot"
    28  	"github.com/snapcore/snapd/overlord/auth"
    29  	"github.com/snapcore/snapd/overlord/snapstate"
    30  	"github.com/snapcore/snapd/overlord/state"
    31  	"github.com/snapcore/snapd/overlord/storecontext"
    32  )
    33  
    34  /*
    35  
    36  This is the central logic to setup and mediate the access to the to-be
    37  device state and dedicated store during remodeling and drive the
    38  re-registration, leveraging the snapstate.DeviceContext/DeviceCtx and
    39  storecontext.DeviceBackend mechanisms and also registrationContext.
    40  
    41  Different context implementations will be used depending on the kind
    42  of remodel, and those will play the roles/implement as needed
    43  snapstate.DeviceContext, storecontext.DeviceBackend and
    44  registrationContext:
    45  
    46  * same brand/model, brand store => updateRemodel
    47    this is just a contextual carrier for the new model
    48  
    49  * same brand/model different brand store => storeSwitchRemodel this
    50    mediates access to device state kept on the remodel change, it also
    51    creates a store that uses that and refers to the new brand store
    52  
    53  * different brand/model, maybe different brand store => reregRemodel
    54    similar to storeSwitchRemodel case after a first phase that performs
    55    re-registration where the context plays registrationContext's role
    56    (NOT IMPLEMENTED YET)
    57  
    58  */
    59  
    60  // RemodelKind designates a kind of remodeling.
    61  type RemodelKind int
    62  
    63  const (
    64  	// same brand/model, brand store
    65  	UpdateRemodel RemodelKind = iota
    66  	// same brand/model, different brand store
    67  	StoreSwitchRemodel
    68  	// different brand/model, maybe different brand store
    69  	ReregRemodel
    70  )
    71  
    72  func (k RemodelKind) String() string {
    73  	switch k {
    74  	case UpdateRemodel:
    75  		return "revision update remodel"
    76  	case StoreSwitchRemodel:
    77  		return "store switch remodel"
    78  	case ReregRemodel:
    79  		return "re-registration remodel"
    80  	}
    81  	panic(fmt.Sprintf("internal error: unknown remodel kind: %d", k))
    82  }
    83  
    84  // ClassifyRemodel returns what kind of remodeling is going from oldModel to newModel.
    85  func ClassifyRemodel(oldModel, newModel *asserts.Model) RemodelKind {
    86  	if oldModel.BrandID() != newModel.BrandID() {
    87  		return ReregRemodel
    88  	}
    89  	if oldModel.Model() != newModel.Model() {
    90  		return ReregRemodel
    91  	}
    92  	if oldModel.Store() != newModel.Store() {
    93  		return StoreSwitchRemodel
    94  	}
    95  	return UpdateRemodel
    96  }
    97  
    98  type remodelCtxKey struct {
    99  	chgID string
   100  }
   101  
   102  func cachedRemodelCtx(chg *state.Change) (remodelContext, bool) {
   103  	key := remodelCtxKey{chg.ID()}
   104  	remodCtx, ok := chg.State().Cached(key).(remodelContext)
   105  	return remodCtx, ok
   106  }
   107  
   108  func cleanupRemodelCtx(chg *state.Change) {
   109  	chg.State().Cache(remodelCtxKey{chg.ID()}, nil)
   110  }
   111  
   112  // A remodelContext mediates the correct and isolated device state
   113  // access and evolution during a remodel.
   114  // All remodelContexts are at least a DeviceContext.
   115  type remodelContext interface {
   116  	Init(chg *state.Change)
   117  	Finish() error
   118  	snapstate.DeviceContext
   119  
   120  	Kind() RemodelKind
   121  
   122  	// initialDevice takes the current/initial device state
   123  	// when setting up the remodel context
   124  	initialDevice(device *auth.DeviceState) error
   125  	// associate associates the remodel context with the change
   126  	// and caches it
   127  	associate(chg *state.Change)
   128  	// setTriedRecoverySystemLabel records the label of a good recovery
   129  	// system created during remodel
   130  	setRecoverySystemLabel(label string)
   131  }
   132  
   133  // remodelCtx returns a remodeling context for the given transition.
   134  // It constructs and caches a dedicated store as needed as well.
   135  func remodelCtx(st *state.State, oldModel, newModel *asserts.Model) (remodelContext, error) {
   136  	var remodCtx remodelContext
   137  
   138  	devMgr := deviceMgr(st)
   139  
   140  	switch kind := ClassifyRemodel(oldModel, newModel); kind {
   141  	case UpdateRemodel:
   142  		// simple context for the simple case
   143  		groundCtx := groundDeviceContext{
   144  			model:      newModel,
   145  			systemMode: devMgr.SystemMode(SysAny),
   146  		}
   147  		remodCtx = &updateRemodelContext{baseRemodelContext{
   148  			groundDeviceContext: groundCtx,
   149  
   150  			oldModel:  oldModel,
   151  			deviceMgr: devMgr,
   152  			st:        st,
   153  		}}
   154  	case StoreSwitchRemodel:
   155  		remodCtx = newNewStoreRemodelContext(st, devMgr, newModel, oldModel)
   156  	case ReregRemodel:
   157  		remodCtx = &reregRemodelContext{
   158  			newStoreRemodelContext: newNewStoreRemodelContext(st, devMgr, newModel, oldModel),
   159  		}
   160  	default:
   161  		return nil, fmt.Errorf("unsupported remodel: %s", kind)
   162  	}
   163  
   164  	device, err := devMgr.device()
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  	if err := remodCtx.initialDevice(device); err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	return remodCtx, nil
   173  }
   174  
   175  // remodelCtxFromTask returns a possibly cached remodeling context associated
   176  // with the task via its change, if task is nil or the task change
   177  // is not a remodeling it will return ErrNoState.
   178  func remodelCtxFromTask(t *state.Task) (remodelContext, error) {
   179  	if t == nil {
   180  		return nil, state.ErrNoState
   181  	}
   182  	chg := t.Change()
   183  	if chg == nil {
   184  		return nil, state.ErrNoState
   185  	}
   186  
   187  	var encNewModel string
   188  	if err := chg.Get("new-model", &encNewModel); err != nil {
   189  		return nil, err
   190  	}
   191  
   192  	// shortcut, cached?
   193  	if remodCtx, ok := cachedRemodelCtx(chg); ok {
   194  		return remodCtx, nil
   195  	}
   196  
   197  	st := t.State()
   198  	oldModel, err := findModel(st)
   199  	if err != nil {
   200  		return nil, fmt.Errorf("internal error: cannot find old model during remodel: %v", err)
   201  	}
   202  	newModelA, err := asserts.Decode([]byte(encNewModel))
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  	newModel, ok := newModelA.(*asserts.Model)
   207  	if !ok {
   208  		return nil, fmt.Errorf("internal error: cannot use a remodel new-model, wrong type")
   209  	}
   210  
   211  	remodCtx, err := remodelCtx(st, oldModel, newModel)
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  	remodCtx.associate(chg)
   216  	return remodCtx, nil
   217  }
   218  
   219  type baseRemodelContext struct {
   220  	// groundDeviceContext will carry the new device model
   221  	groundDeviceContext
   222  	oldModel *asserts.Model
   223  
   224  	deviceMgr *DeviceManager
   225  	st        *state.State
   226  
   227  	recoverySystemLabel string
   228  }
   229  
   230  func (rc *baseRemodelContext) ForRemodeling() bool {
   231  	return true
   232  }
   233  
   234  func (rc *baseRemodelContext) GroundContext() snapstate.DeviceContext {
   235  	return &groundDeviceContext{
   236  		model:      rc.oldModel,
   237  		systemMode: rc.systemMode,
   238  	}
   239  }
   240  
   241  func (rc *baseRemodelContext) initialDevice(*auth.DeviceState) error {
   242  	// do nothing
   243  	return nil
   244  }
   245  
   246  func (rc *baseRemodelContext) cacheViaChange(chg *state.Change, remodCtx remodelContext) {
   247  	chg.State().Cache(remodelCtxKey{chg.ID()}, remodCtx)
   248  }
   249  
   250  func (rc *baseRemodelContext) init(chg *state.Change) {
   251  	chg.Set("new-model", string(asserts.Encode(rc.model)))
   252  }
   253  
   254  func (rc *baseRemodelContext) SystemMode() string {
   255  	return rc.systemMode
   256  }
   257  
   258  func (rc *baseRemodelContext) setRecoverySystemLabel(label string) {
   259  	rc.recoverySystemLabel = label
   260  }
   261  
   262  // updateRunModeSystem updates the device context used during boot and makes a
   263  // record of the new seeded system.
   264  func (rc *baseRemodelContext) updateRunModeSystem() error {
   265  	if rc.model.Grade() == asserts.ModelGradeUnset {
   266  		// nothing special for non-UC20 systems
   267  		return nil
   268  	}
   269  	if rc.recoverySystemLabel == "" {
   270  		return fmt.Errorf("internal error: recovery system label is unset during remodel finish")
   271  	}
   272  	// for UC20 systems we need record the fact that a new model is used for
   273  	// booting and consider a new recovery system as as seeded
   274  	oldDeviceContext := rc.GroundContext()
   275  	newDeviceContext := &rc.groundDeviceContext
   276  	if err := boot.DeviceChange(oldDeviceContext, newDeviceContext); err != nil {
   277  		return fmt.Errorf("cannot switch device: %v", err)
   278  	}
   279  	if err := rc.deviceMgr.recordSeededSystem(rc.st, &seededSystem{
   280  		System:    rc.recoverySystemLabel,
   281  		Model:     rc.model.Model(),
   282  		BrandID:   rc.model.BrandID(),
   283  		Revision:  rc.model.Revision(),
   284  		Timestamp: rc.model.Timestamp(),
   285  		SeedTime:  time.Now(),
   286  	}); err != nil {
   287  		return fmt.Errorf("cannot record a new seeded system: %v", err)
   288  	}
   289  	return nil
   290  }
   291  
   292  // updateRemodelContext: model assertion revision-only update remodel
   293  // (no change to brand/model or store)
   294  type updateRemodelContext struct {
   295  	baseRemodelContext
   296  }
   297  
   298  func (rc *updateRemodelContext) Kind() RemodelKind {
   299  	return UpdateRemodel
   300  }
   301  
   302  func (rc *updateRemodelContext) associate(chg *state.Change) {
   303  	rc.cacheViaChange(chg, rc)
   304  }
   305  
   306  func (rc *updateRemodelContext) Init(chg *state.Change) {
   307  	rc.init(chg)
   308  
   309  	rc.associate(chg)
   310  }
   311  
   312  func (rc *updateRemodelContext) Store() snapstate.StoreService {
   313  	return nil
   314  }
   315  
   316  func (rc *updateRemodelContext) Finish() error {
   317  	// nothing special to do as part of the finish action, so just run the
   318  	// update boot step
   319  	return rc.updateRunModeSystem()
   320  }
   321  
   322  // newStoreRemodelContext: remodel needing a new store session
   323  // (for change of store (or brand/model))
   324  type newStoreRemodelContext struct {
   325  	baseRemodelContext
   326  
   327  	// device state storage before this is associate with a change
   328  	deviceState *auth.DeviceState
   329  	// the associated change
   330  	remodelChange *state.Change
   331  
   332  	store snapstate.StoreService
   333  }
   334  
   335  func newNewStoreRemodelContext(st *state.State, devMgr *DeviceManager, newModel, oldModel *asserts.Model) *newStoreRemodelContext {
   336  	rc := &newStoreRemodelContext{}
   337  	groundCtx := groundDeviceContext{
   338  		model:      newModel,
   339  		systemMode: devMgr.SystemMode(SysAny),
   340  	}
   341  	rc.baseRemodelContext = baseRemodelContext{
   342  		groundDeviceContext: groundCtx,
   343  		oldModel:            oldModel,
   344  
   345  		deviceMgr: devMgr,
   346  		st:        st,
   347  	}
   348  	rc.store = devMgr.newStore(rc.deviceBackend())
   349  	return rc
   350  }
   351  
   352  func (rc *newStoreRemodelContext) Kind() RemodelKind {
   353  	return StoreSwitchRemodel
   354  }
   355  
   356  func (rc *newStoreRemodelContext) associate(chg *state.Change) {
   357  	rc.remodelChange = chg
   358  	rc.cacheViaChange(chg, rc)
   359  }
   360  
   361  func (rc *newStoreRemodelContext) initialDevice(device *auth.DeviceState) error {
   362  	device1 := *device
   363  	// we will need a new one, it might embed the store as well
   364  	device1.SessionMacaroon = ""
   365  	rc.deviceState = &device1
   366  	return nil
   367  }
   368  
   369  func (rc *newStoreRemodelContext) init(chg *state.Change) {
   370  	rc.baseRemodelContext.init(chg)
   371  
   372  	chg.Set("device", rc.deviceState)
   373  	rc.deviceState = nil
   374  }
   375  
   376  func (rc *newStoreRemodelContext) Init(chg *state.Change) {
   377  	rc.init(chg)
   378  
   379  	rc.associate(chg)
   380  }
   381  
   382  func (rc *newStoreRemodelContext) Store() snapstate.StoreService {
   383  	return rc.store
   384  }
   385  
   386  func (rc *newStoreRemodelContext) device() (*auth.DeviceState, error) {
   387  	var err error
   388  	var device auth.DeviceState
   389  	if rc.remodelChange == nil {
   390  		// no remodelChange yet
   391  		device = *rc.deviceState
   392  	} else {
   393  		err = rc.remodelChange.Get("device", &device)
   394  	}
   395  	return &device, err
   396  }
   397  
   398  func (rc *newStoreRemodelContext) setCtxDevice(device *auth.DeviceState) {
   399  	if rc.remodelChange == nil {
   400  		// no remodelChange yet
   401  		rc.deviceState = device
   402  	} else {
   403  		rc.remodelChange.Set("device", device)
   404  	}
   405  }
   406  
   407  func (rc *newStoreRemodelContext) Finish() error {
   408  	// expose the device state of the remodel with the new session
   409  	// to the rest of the system
   410  	remodelDevice, err := rc.device()
   411  	if err != nil {
   412  		return err
   413  	}
   414  	if err := rc.deviceMgr.setDevice(remodelDevice); err != nil {
   415  		return err
   416  	}
   417  	return rc.updateRunModeSystem()
   418  }
   419  
   420  func (rc *newStoreRemodelContext) deviceBackend() storecontext.DeviceBackend {
   421  	return &remodelDeviceBackend{rc}
   422  }
   423  
   424  type remodelDeviceBackend struct {
   425  	*newStoreRemodelContext
   426  }
   427  
   428  func (b remodelDeviceBackend) Device() (*auth.DeviceState, error) {
   429  	return b.device()
   430  }
   431  
   432  func (b remodelDeviceBackend) SetDevice(device *auth.DeviceState) error {
   433  	b.setCtxDevice(device)
   434  	return nil
   435  }
   436  
   437  func (b remodelDeviceBackend) Model() (*asserts.Model, error) {
   438  	return b.model, nil
   439  }
   440  
   441  func (b remodelDeviceBackend) Serial() (*asserts.Serial, error) {
   442  	// this the shared logic, also correct for the rereg case
   443  	// we should lookup the serial with the remodeling device state
   444  	device, err := b.device()
   445  	if err != nil {
   446  		return nil, err
   447  	}
   448  	return findSerial(b.st, device)
   449  }
   450  
   451  // reregRemodelContext: remodel for a change of brand/model
   452  type reregRemodelContext struct {
   453  	*newStoreRemodelContext
   454  
   455  	origModel  *asserts.Model
   456  	origSerial *asserts.Serial
   457  }
   458  
   459  func (rc *reregRemodelContext) Kind() RemodelKind {
   460  	return ReregRemodel
   461  }
   462  
   463  func (rc *reregRemodelContext) associate(chg *state.Change) {
   464  	rc.remodelChange = chg
   465  	rc.cacheViaChange(chg, rc)
   466  }
   467  
   468  func (rc *reregRemodelContext) initialDevice(device *auth.DeviceState) error {
   469  	origModel, err := findModel(rc.st)
   470  	if err != nil {
   471  		return err
   472  	}
   473  	origSerial, err := findSerial(rc.st, nil)
   474  	if err != nil {
   475  		return fmt.Errorf("cannot find current serial before proceeding with re-registration: %v", err)
   476  	}
   477  	rc.origModel = origModel
   478  	rc.origSerial = origSerial
   479  
   480  	// starting almost from scratch with only device-key
   481  	rc.deviceState = &auth.DeviceState{
   482  		Brand: rc.model.BrandID(),
   483  		Model: rc.model.Model(),
   484  		KeyID: device.KeyID,
   485  	}
   486  	return nil
   487  }
   488  
   489  func (rc *reregRemodelContext) Init(chg *state.Change) {
   490  	rc.init(chg)
   491  
   492  	rc.associate(chg)
   493  }
   494  
   495  // reregRemodelContext impl of registrationContext
   496  
   497  func (rc *reregRemodelContext) Device() (*auth.DeviceState, error) {
   498  	return rc.device()
   499  }
   500  
   501  func (rc *reregRemodelContext) GadgetForSerialRequestConfig() string {
   502  	return rc.origModel.Gadget()
   503  }
   504  
   505  func (rc *reregRemodelContext) SerialRequestExtraHeaders() map[string]interface{} {
   506  	return map[string]interface{}{
   507  		"original-brand-id": rc.origSerial.BrandID(),
   508  		"original-model":    rc.origSerial.Model(),
   509  		"original-serial":   rc.origSerial.Serial(),
   510  	}
   511  }
   512  
   513  func (rc *reregRemodelContext) SerialRequestAncillaryAssertions() []asserts.Assertion {
   514  	return []asserts.Assertion{rc.model, rc.origSerial}
   515  }
   516  
   517  func (rc *reregRemodelContext) FinishRegistration(serial *asserts.Serial) error {
   518  	device, err := rc.device()
   519  	if err != nil {
   520  		return err
   521  	}
   522  
   523  	device.Serial = serial.Serial()
   524  	rc.setCtxDevice(device)
   525  	return nil
   526  }