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 }