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 }