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 }