github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/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  
    25  	"github.com/snapcore/snapd/asserts"
    26  	"github.com/snapcore/snapd/overlord/auth"
    27  	"github.com/snapcore/snapd/overlord/snapstate"
    28  	"github.com/snapcore/snapd/overlord/state"
    29  	"github.com/snapcore/snapd/overlord/storecontext"
    30  )
    31  
    32  /*
    33  
    34  This is the central logic to setup and mediate the access to the to-be
    35  device state and dedicated store during remodeling and drive the
    36  re-registration, leveraging the snapstate.DeviceContext/DeviceCtx and
    37  storecontext.DeviceBackend mechanisms and also registrationContext.
    38  
    39  Different context implementations will be used depending on the kind
    40  of remodel, and those will play the roles/implement as needed
    41  snapstate.DeviceContext, storecontext.DeviceBackend and
    42  registrationContext:
    43  
    44  * same brand/model, brand store => updateRemodel
    45    this is just a contextual carrier for the new model
    46  
    47  * same brand/model different brand store => storeSwitchRemodel this
    48    mediates access to device state kept on the remodel change, it also
    49    creates a store that uses that and refers to the new brand store
    50  
    51  * different brand/model, maybe different brand store => reregRemodel
    52    similar to storeSwitchRemodel case after a first phase that performs
    53    re-registration where the context plays registrationContext's role
    54    (NOT IMPLEMENTED YET)
    55  
    56  */
    57  
    58  // RemodelKind designates a kind of remodeling.
    59  type RemodelKind int
    60  
    61  const (
    62  	// same brand/model, brand store
    63  	UpdateRemodel RemodelKind = iota
    64  	// same brand/model, different brand store
    65  	StoreSwitchRemodel
    66  	// different brand/model, maybe different brand store
    67  	ReregRemodel
    68  )
    69  
    70  func (k RemodelKind) String() string {
    71  	switch k {
    72  	case UpdateRemodel:
    73  		return "revision update remodel"
    74  	case StoreSwitchRemodel:
    75  		return "store switch remodel"
    76  	case ReregRemodel:
    77  		return "re-registration remodel"
    78  	}
    79  	panic(fmt.Sprintf("internal error: unknown remodel kind: %d", k))
    80  }
    81  
    82  // ClassifyRemodel returns what kind of remodeling is going from oldModel to newModel.
    83  func ClassifyRemodel(oldModel, newModel *asserts.Model) RemodelKind {
    84  	if oldModel.BrandID() != newModel.BrandID() {
    85  		return ReregRemodel
    86  	}
    87  	if oldModel.Model() != newModel.Model() {
    88  		return ReregRemodel
    89  	}
    90  	if oldModel.Store() != newModel.Store() {
    91  		return StoreSwitchRemodel
    92  	}
    93  	return UpdateRemodel
    94  }
    95  
    96  type remodelCtxKey struct {
    97  	chgID string
    98  }
    99  
   100  func cachedRemodelCtx(chg *state.Change) (remodelContext, bool) {
   101  	key := remodelCtxKey{chg.ID()}
   102  	remodCtx, ok := chg.State().Cached(key).(remodelContext)
   103  	return remodCtx, ok
   104  }
   105  
   106  func cleanupRemodelCtx(chg *state.Change) {
   107  	chg.State().Cache(remodelCtxKey{chg.ID()}, nil)
   108  }
   109  
   110  // A remodelContext mediates the correct and isolated device state
   111  // access and evolution during a remodel.
   112  // All remodelContexts are at least a DeviceContext.
   113  type remodelContext interface {
   114  	Init(chg *state.Change)
   115  	Finish() error
   116  	snapstate.DeviceContext
   117  
   118  	Kind() RemodelKind
   119  
   120  	// initialDevice takes the current/initial device state
   121  	// when setting up the remodel context
   122  	initialDevice(device *auth.DeviceState) error
   123  	// associate associates the remodel context with the change
   124  	// and caches it
   125  	associate(chg *state.Change)
   126  }
   127  
   128  // remodelCtx returns a remodeling context for the given transition.
   129  // It constructs and caches a dedicated store as needed as well.
   130  func remodelCtx(st *state.State, oldModel, newModel *asserts.Model) (remodelContext, error) {
   131  	var remodCtx remodelContext
   132  
   133  	devMgr := deviceMgr(st)
   134  
   135  	switch kind := ClassifyRemodel(oldModel, newModel); kind {
   136  	case UpdateRemodel:
   137  		// simple context for the simple case
   138  		groundCtx := groundDeviceContext{
   139  			model:      newModel,
   140  			systemMode: devMgr.SystemMode(SysAny),
   141  		}
   142  		remodCtx = &updateRemodelContext{baseRemodelContext{groundCtx, oldModel}}
   143  	case StoreSwitchRemodel:
   144  		remodCtx = newNewStoreRemodelContext(st, devMgr, newModel, oldModel)
   145  	case ReregRemodel:
   146  		remodCtx = &reregRemodelContext{
   147  			newStoreRemodelContext: newNewStoreRemodelContext(st, devMgr, newModel, oldModel),
   148  		}
   149  	default:
   150  		return nil, fmt.Errorf("unsupported remodel: %s", kind)
   151  	}
   152  
   153  	device, err := devMgr.device()
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  	if err := remodCtx.initialDevice(device); err != nil {
   158  		return nil, err
   159  	}
   160  
   161  	return remodCtx, nil
   162  }
   163  
   164  // remodelCtxFromTask returns a possibly cached remodeling context associated
   165  // with the task via its change, if task is nil or the task change
   166  // is not a remodeling it will return ErrNoState.
   167  func remodelCtxFromTask(t *state.Task) (remodelContext, error) {
   168  	if t == nil {
   169  		return nil, state.ErrNoState
   170  	}
   171  	chg := t.Change()
   172  	if chg == nil {
   173  		return nil, state.ErrNoState
   174  	}
   175  
   176  	var encNewModel string
   177  	if err := chg.Get("new-model", &encNewModel); err != nil {
   178  		return nil, err
   179  	}
   180  
   181  	// shortcut, cached?
   182  	if remodCtx, ok := cachedRemodelCtx(chg); ok {
   183  		return remodCtx, nil
   184  	}
   185  
   186  	st := t.State()
   187  	oldModel, err := findModel(st)
   188  	if err != nil {
   189  		return nil, fmt.Errorf("internal error: cannot find old model during remodel: %v", err)
   190  	}
   191  	newModelA, err := asserts.Decode([]byte(encNewModel))
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  	newModel, ok := newModelA.(*asserts.Model)
   196  	if !ok {
   197  		return nil, fmt.Errorf("internal error: cannot use a remodel new-model, wrong type")
   198  	}
   199  
   200  	remodCtx, err := remodelCtx(st, oldModel, newModel)
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  	remodCtx.associate(chg)
   205  	return remodCtx, nil
   206  }
   207  
   208  type baseRemodelContext struct {
   209  	groundDeviceContext
   210  	oldModel *asserts.Model
   211  }
   212  
   213  func (rc *baseRemodelContext) ForRemodeling() bool {
   214  	return true
   215  }
   216  
   217  func (rc *baseRemodelContext) GroundContext() snapstate.DeviceContext {
   218  	return &groundDeviceContext{
   219  		model:      rc.oldModel,
   220  		systemMode: rc.systemMode,
   221  	}
   222  }
   223  
   224  func (rc *baseRemodelContext) initialDevice(*auth.DeviceState) error {
   225  	// do nothing
   226  	return nil
   227  }
   228  
   229  func (rc *baseRemodelContext) cacheViaChange(chg *state.Change, remodCtx remodelContext) {
   230  	chg.State().Cache(remodelCtxKey{chg.ID()}, remodCtx)
   231  }
   232  
   233  func (rc *baseRemodelContext) init(chg *state.Change) {
   234  	chg.Set("new-model", string(asserts.Encode(rc.model)))
   235  }
   236  
   237  func (rc *baseRemodelContext) SystemMode() string {
   238  	return rc.systemMode
   239  }
   240  
   241  // updateRemodelContext: model assertion revision-only update remodel
   242  // (no change to brand/model or store)
   243  type updateRemodelContext struct {
   244  	baseRemodelContext
   245  }
   246  
   247  func (rc *updateRemodelContext) Kind() RemodelKind {
   248  	return UpdateRemodel
   249  }
   250  
   251  func (rc *updateRemodelContext) associate(chg *state.Change) {
   252  	rc.cacheViaChange(chg, rc)
   253  }
   254  
   255  func (rc *updateRemodelContext) Init(chg *state.Change) {
   256  	rc.init(chg)
   257  
   258  	rc.associate(chg)
   259  }
   260  
   261  func (rc *updateRemodelContext) Store() snapstate.StoreService {
   262  	return nil
   263  }
   264  
   265  func (rc *updateRemodelContext) Finish() error {
   266  	// nothing more to do
   267  	return nil
   268  }
   269  
   270  // newStoreRemodelContext: remodel needing a new store session
   271  // (for change of store (or brand/model))
   272  type newStoreRemodelContext struct {
   273  	baseRemodelContext
   274  
   275  	// device state storage before this is associate with a change
   276  	deviceState *auth.DeviceState
   277  	// the associated change
   278  	remodelChange *state.Change
   279  
   280  	store snapstate.StoreService
   281  
   282  	st        *state.State
   283  	deviceMgr *DeviceManager
   284  }
   285  
   286  func newNewStoreRemodelContext(st *state.State, devMgr *DeviceManager, newModel, oldModel *asserts.Model) *newStoreRemodelContext {
   287  	rc := &newStoreRemodelContext{}
   288  	groundCtx := groundDeviceContext{
   289  		model:      newModel,
   290  		systemMode: devMgr.SystemMode(SysAny),
   291  	}
   292  	rc.baseRemodelContext = baseRemodelContext{groundCtx, oldModel}
   293  	rc.st = st
   294  	rc.deviceMgr = devMgr
   295  	rc.store = devMgr.newStore(rc.deviceBackend())
   296  	return rc
   297  }
   298  
   299  func (rc *newStoreRemodelContext) Kind() RemodelKind {
   300  	return StoreSwitchRemodel
   301  }
   302  
   303  func (rc *newStoreRemodelContext) associate(chg *state.Change) {
   304  	rc.remodelChange = chg
   305  	rc.cacheViaChange(chg, rc)
   306  }
   307  
   308  func (rc *newStoreRemodelContext) initialDevice(device *auth.DeviceState) error {
   309  	device1 := *device
   310  	// we will need a new one, it might embed the store as well
   311  	device1.SessionMacaroon = ""
   312  	rc.deviceState = &device1
   313  	return nil
   314  }
   315  
   316  func (rc *newStoreRemodelContext) init(chg *state.Change) {
   317  	rc.baseRemodelContext.init(chg)
   318  
   319  	chg.Set("device", rc.deviceState)
   320  	rc.deviceState = nil
   321  }
   322  
   323  func (rc *newStoreRemodelContext) Init(chg *state.Change) {
   324  	rc.init(chg)
   325  
   326  	rc.associate(chg)
   327  }
   328  
   329  func (rc *newStoreRemodelContext) Store() snapstate.StoreService {
   330  	return rc.store
   331  }
   332  
   333  func (rc *newStoreRemodelContext) device() (*auth.DeviceState, error) {
   334  	var err error
   335  	var device auth.DeviceState
   336  	if rc.remodelChange == nil {
   337  		// no remodelChange yet
   338  		device = *rc.deviceState
   339  	} else {
   340  		err = rc.remodelChange.Get("device", &device)
   341  	}
   342  	return &device, err
   343  }
   344  
   345  func (rc *newStoreRemodelContext) setCtxDevice(device *auth.DeviceState) {
   346  	if rc.remodelChange == nil {
   347  		// no remodelChange yet
   348  		rc.deviceState = device
   349  	} else {
   350  		rc.remodelChange.Set("device", device)
   351  	}
   352  }
   353  
   354  func (rc *newStoreRemodelContext) Finish() error {
   355  	// expose the device state of the remodel with the new session
   356  	// to the rest of the system
   357  	remodelDevice, err := rc.device()
   358  	if err != nil {
   359  		return err
   360  	}
   361  	return rc.deviceMgr.setDevice(remodelDevice)
   362  }
   363  
   364  func (rc *newStoreRemodelContext) deviceBackend() storecontext.DeviceBackend {
   365  	return &remodelDeviceBackend{rc}
   366  }
   367  
   368  type remodelDeviceBackend struct {
   369  	*newStoreRemodelContext
   370  }
   371  
   372  func (b remodelDeviceBackend) Device() (*auth.DeviceState, error) {
   373  	return b.device()
   374  }
   375  
   376  func (b remodelDeviceBackend) SetDevice(device *auth.DeviceState) error {
   377  	b.setCtxDevice(device)
   378  	return nil
   379  }
   380  
   381  func (b remodelDeviceBackend) Model() (*asserts.Model, error) {
   382  	return b.model, nil
   383  }
   384  
   385  func (b remodelDeviceBackend) Serial() (*asserts.Serial, error) {
   386  	// this the shared logic, also correct for the rereg case
   387  	// we should lookup the serial with the remodeling device state
   388  	device, err := b.device()
   389  	if err != nil {
   390  		return nil, err
   391  	}
   392  	return findSerial(b.st, device)
   393  }
   394  
   395  // reregRemodelContext: remodel for a change of brand/model
   396  type reregRemodelContext struct {
   397  	*newStoreRemodelContext
   398  
   399  	origModel  *asserts.Model
   400  	origSerial *asserts.Serial
   401  }
   402  
   403  func (rc *reregRemodelContext) Kind() RemodelKind {
   404  	return ReregRemodel
   405  }
   406  
   407  func (rc *reregRemodelContext) associate(chg *state.Change) {
   408  	rc.remodelChange = chg
   409  	rc.cacheViaChange(chg, rc)
   410  }
   411  
   412  func (rc *reregRemodelContext) initialDevice(device *auth.DeviceState) error {
   413  	origModel, err := findModel(rc.st)
   414  	if err != nil {
   415  		return err
   416  	}
   417  	origSerial, err := findSerial(rc.st, nil)
   418  	if err != nil {
   419  		return fmt.Errorf("cannot find current serial before proceeding with re-registration: %v", err)
   420  	}
   421  	rc.origModel = origModel
   422  	rc.origSerial = origSerial
   423  
   424  	// starting almost from scratch with only device-key
   425  	rc.deviceState = &auth.DeviceState{
   426  		Brand: rc.model.BrandID(),
   427  		Model: rc.model.Model(),
   428  		KeyID: device.KeyID,
   429  	}
   430  	return nil
   431  }
   432  
   433  func (rc *reregRemodelContext) Init(chg *state.Change) {
   434  	rc.init(chg)
   435  
   436  	rc.associate(chg)
   437  }
   438  
   439  // reregRemodelContext impl of registrationContext
   440  
   441  func (rc *reregRemodelContext) Device() (*auth.DeviceState, error) {
   442  	return rc.device()
   443  }
   444  
   445  func (rc *reregRemodelContext) GadgetForSerialRequestConfig() string {
   446  	return rc.origModel.Gadget()
   447  }
   448  
   449  func (rc *reregRemodelContext) SerialRequestExtraHeaders() map[string]interface{} {
   450  	return map[string]interface{}{
   451  		"original-brand-id": rc.origSerial.BrandID(),
   452  		"original-model":    rc.origSerial.Model(),
   453  		"original-serial":   rc.origSerial.Serial(),
   454  	}
   455  }
   456  
   457  func (rc *reregRemodelContext) SerialRequestAncillaryAssertions() []asserts.Assertion {
   458  	return []asserts.Assertion{rc.model, rc.origSerial}
   459  }
   460  
   461  func (rc *reregRemodelContext) FinishRegistration(serial *asserts.Serial) error {
   462  	device, err := rc.device()
   463  	if err != nil {
   464  		return err
   465  	}
   466  
   467  	device.Serial = serial.Serial()
   468  	rc.setCtxDevice(device)
   469  	return nil
   470  }