github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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  		remodCtx = &updateRemodelContext{baseRemodelContext{newModel}}
   139  	case StoreSwitchRemodel:
   140  		remodCtx = newNewStoreRemodelContext(st, devMgr, newModel)
   141  	case ReregRemodel:
   142  		remodCtx = &reregRemodelContext{
   143  			newStoreRemodelContext: newNewStoreRemodelContext(st, devMgr, newModel),
   144  		}
   145  	default:
   146  		return nil, fmt.Errorf("unsupported remodel: %s", kind)
   147  	}
   148  
   149  	device, err := devMgr.device()
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  	if err := remodCtx.initialDevice(device); err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	return remodCtx, nil
   158  }
   159  
   160  // remodelCtxFromTask returns a possibly cached remodeling context associated
   161  // with the task via its change, if task is nil or the task change
   162  // is not a remodeling it will return ErrNoState.
   163  func remodelCtxFromTask(t *state.Task) (remodelContext, error) {
   164  	if t == nil {
   165  		return nil, state.ErrNoState
   166  	}
   167  	chg := t.Change()
   168  	if chg == nil {
   169  		return nil, state.ErrNoState
   170  	}
   171  
   172  	var encNewModel string
   173  	if err := chg.Get("new-model", &encNewModel); err != nil {
   174  		return nil, err
   175  	}
   176  
   177  	// shortcut, cached?
   178  	if remodCtx, ok := cachedRemodelCtx(chg); ok {
   179  		return remodCtx, nil
   180  	}
   181  
   182  	st := t.State()
   183  	oldModel, err := findModel(st)
   184  	if err != nil {
   185  		return nil, fmt.Errorf("internal error: cannot find old model during remodel: %v", err)
   186  	}
   187  	newModelA, err := asserts.Decode([]byte(encNewModel))
   188  	if err != nil {
   189  		return nil, err
   190  	}
   191  	newModel, ok := newModelA.(*asserts.Model)
   192  	if !ok {
   193  		return nil, fmt.Errorf("internal error: cannot use a remodel new-model, wrong type")
   194  	}
   195  
   196  	remodCtx, err := remodelCtx(st, oldModel, newModel)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	remodCtx.associate(chg)
   201  	return remodCtx, nil
   202  }
   203  
   204  type baseRemodelContext struct {
   205  	newModel *asserts.Model
   206  }
   207  
   208  func (rc baseRemodelContext) ForRemodeling() bool {
   209  	return true
   210  }
   211  
   212  func (rc baseRemodelContext) Model() *asserts.Model {
   213  	return rc.newModel
   214  }
   215  
   216  func (rc baseRemodelContext) initialDevice(*auth.DeviceState) error {
   217  	// do nothing
   218  	return nil
   219  }
   220  
   221  func (rc baseRemodelContext) cacheViaChange(chg *state.Change, remodCtx remodelContext) {
   222  	chg.State().Cache(remodelCtxKey{chg.ID()}, remodCtx)
   223  }
   224  
   225  func (rc baseRemodelContext) init(chg *state.Change) {
   226  	chg.Set("new-model", string(asserts.Encode(rc.newModel)))
   227  }
   228  
   229  // updateRemodelContext: model assertion revision-only update remodel
   230  // (no change to brand/model or store)
   231  type updateRemodelContext struct {
   232  	baseRemodelContext
   233  }
   234  
   235  func (rc *updateRemodelContext) Kind() RemodelKind {
   236  	return UpdateRemodel
   237  }
   238  
   239  func (rc *updateRemodelContext) associate(chg *state.Change) {
   240  	rc.cacheViaChange(chg, rc)
   241  }
   242  
   243  func (rc *updateRemodelContext) Init(chg *state.Change) {
   244  	rc.init(chg)
   245  
   246  	rc.associate(chg)
   247  }
   248  
   249  func (rc *updateRemodelContext) Store() snapstate.StoreService {
   250  	return nil
   251  }
   252  
   253  func (rc *updateRemodelContext) Finish() error {
   254  	// nothing more to do
   255  	return nil
   256  }
   257  
   258  // newStoreRemodelContext: remodel needing a new store session
   259  // (for change of store (or brand/model))
   260  type newStoreRemodelContext struct {
   261  	baseRemodelContext
   262  
   263  	// device state storage before this is associate with a change
   264  	deviceState *auth.DeviceState
   265  	// the associated change
   266  	remodelChange *state.Change
   267  
   268  	store snapstate.StoreService
   269  
   270  	st        *state.State
   271  	deviceMgr *DeviceManager
   272  }
   273  
   274  func newNewStoreRemodelContext(st *state.State, devMgr *DeviceManager, newModel *asserts.Model) *newStoreRemodelContext {
   275  	rc := &newStoreRemodelContext{}
   276  	rc.baseRemodelContext = baseRemodelContext{newModel}
   277  	rc.st = st
   278  	rc.deviceMgr = devMgr
   279  	rc.store = devMgr.newStore(rc.deviceBackend())
   280  	return rc
   281  }
   282  
   283  func (rc *newStoreRemodelContext) Kind() RemodelKind {
   284  	return StoreSwitchRemodel
   285  }
   286  
   287  func (rc *newStoreRemodelContext) associate(chg *state.Change) {
   288  	rc.remodelChange = chg
   289  	rc.cacheViaChange(chg, rc)
   290  }
   291  
   292  func (rc *newStoreRemodelContext) initialDevice(device *auth.DeviceState) error {
   293  	device1 := *device
   294  	// we will need a new one, it might embed the store as well
   295  	device1.SessionMacaroon = ""
   296  	rc.deviceState = &device1
   297  	return nil
   298  }
   299  
   300  func (rc *newStoreRemodelContext) init(chg *state.Change) {
   301  	rc.baseRemodelContext.init(chg)
   302  
   303  	chg.Set("device", rc.deviceState)
   304  	rc.deviceState = nil
   305  }
   306  
   307  func (rc *newStoreRemodelContext) Init(chg *state.Change) {
   308  	rc.init(chg)
   309  
   310  	rc.associate(chg)
   311  }
   312  
   313  func (rc *newStoreRemodelContext) Store() snapstate.StoreService {
   314  	return rc.store
   315  }
   316  
   317  func (rc *newStoreRemodelContext) device() (*auth.DeviceState, error) {
   318  	var err error
   319  	var device auth.DeviceState
   320  	if rc.remodelChange == nil {
   321  		// no remodelChange yet
   322  		device = *rc.deviceState
   323  	} else {
   324  		err = rc.remodelChange.Get("device", &device)
   325  	}
   326  	return &device, err
   327  }
   328  
   329  func (rc *newStoreRemodelContext) setCtxDevice(device *auth.DeviceState) {
   330  	if rc.remodelChange == nil {
   331  		// no remodelChange yet
   332  		rc.deviceState = device
   333  	} else {
   334  		rc.remodelChange.Set("device", device)
   335  	}
   336  }
   337  
   338  func (rc *newStoreRemodelContext) Finish() error {
   339  	// expose the device state of the remodel with the new session
   340  	// to the rest of the system
   341  	remodelDevice, err := rc.device()
   342  	if err != nil {
   343  		return err
   344  	}
   345  	return rc.deviceMgr.setDevice(remodelDevice)
   346  }
   347  
   348  func (rc *newStoreRemodelContext) deviceBackend() storecontext.DeviceBackend {
   349  	return &remodelDeviceBackend{rc}
   350  }
   351  
   352  type remodelDeviceBackend struct {
   353  	*newStoreRemodelContext
   354  }
   355  
   356  func (b remodelDeviceBackend) Device() (*auth.DeviceState, error) {
   357  	return b.device()
   358  }
   359  
   360  func (b remodelDeviceBackend) SetDevice(device *auth.DeviceState) error {
   361  	b.setCtxDevice(device)
   362  	return nil
   363  }
   364  
   365  func (b remodelDeviceBackend) Model() (*asserts.Model, error) {
   366  	return b.newModel, nil
   367  }
   368  
   369  func (b remodelDeviceBackend) Serial() (*asserts.Serial, error) {
   370  	// this the shared logic, also correct for the rereg case
   371  	// we should lookup the serial with the remodeling device state
   372  	device, err := b.device()
   373  	if err != nil {
   374  		return nil, err
   375  	}
   376  	return findSerial(b.st, device)
   377  }
   378  
   379  // reregRemodelContext: remodel for a change of brand/model
   380  type reregRemodelContext struct {
   381  	*newStoreRemodelContext
   382  
   383  	origModel  *asserts.Model
   384  	origSerial *asserts.Serial
   385  }
   386  
   387  func (rc *reregRemodelContext) Kind() RemodelKind {
   388  	return ReregRemodel
   389  }
   390  
   391  func (rc *reregRemodelContext) associate(chg *state.Change) {
   392  	rc.remodelChange = chg
   393  	rc.cacheViaChange(chg, rc)
   394  }
   395  
   396  func (rc *reregRemodelContext) initialDevice(device *auth.DeviceState) error {
   397  	origModel, err := findModel(rc.st)
   398  	if err != nil {
   399  		return err
   400  	}
   401  	origSerial, err := findSerial(rc.st, nil)
   402  	if err != nil {
   403  		return fmt.Errorf("cannot find current serial before proceeding with re-registration: %v", err)
   404  	}
   405  	rc.origModel = origModel
   406  	rc.origSerial = origSerial
   407  
   408  	// starting almost from scratch with only device-key
   409  	rc.deviceState = &auth.DeviceState{
   410  		Brand: rc.newModel.BrandID(),
   411  		Model: rc.newModel.Model(),
   412  		KeyID: device.KeyID,
   413  	}
   414  	return nil
   415  }
   416  
   417  func (rc *reregRemodelContext) Init(chg *state.Change) {
   418  	rc.init(chg)
   419  
   420  	rc.associate(chg)
   421  }
   422  
   423  // reregRemodelContext impl of registrationContext
   424  
   425  func (rc *reregRemodelContext) Device() (*auth.DeviceState, error) {
   426  	return rc.device()
   427  }
   428  
   429  func (rc *reregRemodelContext) GadgetForSerialRequestConfig() string {
   430  	return rc.origModel.Gadget()
   431  }
   432  
   433  func (rc *reregRemodelContext) SerialRequestExtraHeaders() map[string]interface{} {
   434  	return map[string]interface{}{
   435  		"original-brand-id": rc.origSerial.BrandID(),
   436  		"original-model":    rc.origSerial.Model(),
   437  		"original-serial":   rc.origSerial.Serial(),
   438  	}
   439  }
   440  
   441  func (rc *reregRemodelContext) SerialRequestAncillaryAssertions() []asserts.Assertion {
   442  	return []asserts.Assertion{rc.newModel, rc.origSerial}
   443  }
   444  
   445  func (rc *reregRemodelContext) FinishRegistration(serial *asserts.Serial) error {
   446  	device, err := rc.device()
   447  	if err != nil {
   448  		return err
   449  	}
   450  
   451  	device.Serial = serial.Serial()
   452  	rc.setCtxDevice(device)
   453  	return nil
   454  }