github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/overlord/devicestate/devicestate_remodel_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016-2021 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_test 21 22 import ( 23 "context" 24 "errors" 25 "fmt" 26 "io/ioutil" 27 "path/filepath" 28 "reflect" 29 30 . "gopkg.in/check.v1" 31 "gopkg.in/tomb.v2" 32 33 "github.com/snapcore/snapd/asserts" 34 "github.com/snapcore/snapd/asserts/assertstest" 35 "github.com/snapcore/snapd/gadget" 36 "github.com/snapcore/snapd/gadget/quantity" 37 "github.com/snapcore/snapd/overlord/assertstate" 38 "github.com/snapcore/snapd/overlord/assertstate/assertstatetest" 39 "github.com/snapcore/snapd/overlord/auth" 40 "github.com/snapcore/snapd/overlord/devicestate" 41 "github.com/snapcore/snapd/overlord/devicestate/devicestatetest" 42 "github.com/snapcore/snapd/overlord/snapstate" 43 "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" 44 "github.com/snapcore/snapd/overlord/state" 45 "github.com/snapcore/snapd/overlord/storecontext" 46 "github.com/snapcore/snapd/release" 47 "github.com/snapcore/snapd/snap" 48 "github.com/snapcore/snapd/snap/snapfile" 49 "github.com/snapcore/snapd/snap/snaptest" 50 "github.com/snapcore/snapd/store/storetest" 51 ) 52 53 type deviceMgrRemodelSuite struct { 54 deviceMgrBaseSuite 55 } 56 57 var _ = Suite(&deviceMgrRemodelSuite{}) 58 59 func (s *deviceMgrRemodelSuite) TestRemodelUnhappyNotSeeded(c *C) { 60 s.state.Lock() 61 defer s.state.Unlock() 62 s.state.Set("seeded", false) 63 64 newModel := s.brands.Model("canonical", "pc", map[string]interface{}{ 65 "architecture": "amd64", 66 "kernel": "pc-kernel", 67 "gadget": "pc", 68 }) 69 _, err := devicestate.Remodel(s.state, newModel) 70 c.Assert(err, ErrorMatches, "cannot remodel until fully seeded") 71 } 72 73 var mockCore20ModelHeaders = map[string]interface{}{ 74 "brand": "canonical", 75 "model": "pc-model-20", 76 "architecture": "amd64", 77 "grade": "dangerous", 78 "base": "core20", 79 "snaps": mockCore20ModelSnaps, 80 } 81 82 var mockCore20ModelSnaps = []interface{}{ 83 map[string]interface{}{ 84 "name": "pc-kernel", 85 "id": "pckernelidididididididididididid", 86 "type": "kernel", 87 "default-channel": "20", 88 }, 89 map[string]interface{}{ 90 "name": "pc", 91 "id": "pcididididididididididididididid", 92 "type": "gadget", 93 "default-channel": "20", 94 }, 95 } 96 97 // copy current model unless new model test data is different 98 // and delete nil keys in new model 99 func mergeMockModelHeaders(cur, new map[string]interface{}) { 100 for k, v := range cur { 101 if v, ok := new[k]; ok { 102 if v == nil { 103 delete(new, k) 104 } 105 continue 106 } 107 new[k] = v 108 } 109 } 110 111 func (s *deviceMgrRemodelSuite) TestRemodelUnhappy(c *C) { 112 s.state.Lock() 113 defer s.state.Unlock() 114 s.state.Set("seeded", true) 115 116 // set a model assertion 117 cur := map[string]interface{}{ 118 "brand": "canonical", 119 "model": "pc-model", 120 "architecture": "amd64", 121 "kernel": "pc-kernel", 122 "gadget": "pc", 123 } 124 s.makeModelAssertionInState(c, cur["brand"].(string), cur["model"].(string), map[string]interface{}{ 125 "architecture": cur["architecture"], 126 "kernel": cur["kernel"], 127 "gadget": cur["gadget"], 128 }) 129 s.makeSerialAssertionInState(c, cur["brand"].(string), cur["model"].(string), "orig-serial") 130 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 131 Brand: cur["brand"].(string), 132 Model: cur["model"].(string), 133 Serial: "orig-serial", 134 }) 135 136 // ensure all error cases are checked 137 for _, t := range []struct { 138 new map[string]interface{} 139 errStr string 140 }{ 141 {map[string]interface{}{"architecture": "pdp-7"}, "cannot remodel to different architectures yet"}, 142 {map[string]interface{}{"base": "core18"}, "cannot remodel from core to bases yet"}, 143 {map[string]interface{}{"base": "core20", "kernel": nil, "gadget": nil, "snaps": mockCore20ModelSnaps}, "cannot remodel to Ubuntu Core 20 models yet"}, 144 } { 145 mergeMockModelHeaders(cur, t.new) 146 new := s.brands.Model(t.new["brand"].(string), t.new["model"].(string), t.new) 147 chg, err := devicestate.Remodel(s.state, new) 148 c.Check(chg, IsNil) 149 c.Check(err, ErrorMatches, t.errStr) 150 } 151 } 152 153 func (s *deviceMgrRemodelSuite) TestRemodelNoUC20RemodelYet(c *C) { 154 s.state.Lock() 155 defer s.state.Unlock() 156 s.state.Set("seeded", true) 157 158 // set a model assertion 159 cur := mockCore20ModelHeaders 160 s.makeModelAssertionInState(c, cur["brand"].(string), cur["model"].(string), map[string]interface{}{ 161 "architecture": cur["architecture"], 162 "base": cur["base"], 163 "grade": cur["grade"], 164 "snaps": cur["snaps"], 165 }) 166 s.makeSerialAssertionInState(c, cur["brand"].(string), cur["model"].(string), "orig-serial") 167 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 168 Brand: cur["brand"].(string), 169 Model: cur["model"].(string), 170 Serial: "orig-serial", 171 }) 172 173 // ensure all error cases are checked 174 for _, t := range []struct { 175 new map[string]interface{} 176 errStr string 177 }{ 178 // uc20 model 179 {map[string]interface{}{"grade": "signed"}, "cannot remodel Ubuntu Core 20 models yet"}, 180 {map[string]interface{}{"base": "core22"}, "cannot remodel Ubuntu Core 20 models yet"}, 181 // non-uc20 model 182 {map[string]interface{}{"snaps": nil, "grade": nil, "base": "core", "gadget": "pc", "kernel": "pc-kernel"}, "cannot remodel Ubuntu Core 20 models yet"}, 183 } { 184 mergeMockModelHeaders(cur, t.new) 185 new := s.brands.Model(t.new["brand"].(string), t.new["model"].(string), t.new) 186 chg, err := devicestate.Remodel(s.state, new) 187 c.Check(chg, IsNil) 188 c.Check(err, ErrorMatches, t.errStr) 189 } 190 } 191 192 func (s *deviceMgrRemodelSuite) TestRemodelRequiresSerial(c *C) { 193 s.state.Lock() 194 defer s.state.Unlock() 195 s.state.Set("seeded", true) 196 197 // set a model assertion 198 cur := map[string]interface{}{ 199 "brand": "canonical", 200 "model": "pc-model", 201 "architecture": "amd64", 202 "kernel": "pc-kernel", 203 "gadget": "pc", 204 } 205 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 206 "architecture": "amd64", 207 "kernel": "pc-kernel", 208 "gadget": "pc", 209 }) 210 // no serial assertion, no serial in state 211 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 212 Brand: "canonical", 213 Model: "pc-model", 214 }) 215 216 newModelHdrs := map[string]interface{}{ 217 "revision": "2", 218 } 219 mergeMockModelHeaders(cur, newModelHdrs) 220 new := s.brands.Model("canonical", "pc-model", newModelHdrs) 221 chg, err := devicestate.Remodel(s.state, new) 222 c.Check(chg, IsNil) 223 c.Check(err, ErrorMatches, "cannot remodel without a serial") 224 } 225 226 func (s *deviceMgrRemodelSuite) TestRemodelTasksSwitchGadgetTrack(c *C) { 227 s.testRemodelTasksSwitchTrack(c, "pc", map[string]interface{}{ 228 "gadget": "pc=18", 229 }) 230 } 231 232 func (s *deviceMgrRemodelSuite) TestRemodelTasksSwitchKernelTrack(c *C) { 233 s.testRemodelTasksSwitchTrack(c, "pc-kernel", map[string]interface{}{ 234 "kernel": "pc-kernel=18", 235 }) 236 } 237 238 func (s *deviceMgrRemodelSuite) testRemodelTasksSwitchTrack(c *C, whatRefreshes string, newModelOverrides map[string]interface{}) { 239 s.state.Lock() 240 defer s.state.Unlock() 241 s.state.Set("seeded", true) 242 s.state.Set("refresh-privacy-key", "some-privacy-key") 243 244 var testDeviceCtx snapstate.DeviceContext 245 246 restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { 247 c.Check(flags.Required, Equals, true) 248 c.Check(deviceCtx, Equals, testDeviceCtx) 249 c.Check(fromChange, Equals, "99") 250 251 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 252 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 253 tValidate.WaitFor(tDownload) 254 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 255 tInstall.WaitFor(tValidate) 256 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 257 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 258 return ts, nil 259 }) 260 defer restore() 261 262 restore = devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { 263 c.Check(flags.Required, Equals, false) 264 c.Check(flags.NoReRefresh, Equals, true) 265 c.Check(deviceCtx, Equals, testDeviceCtx) 266 c.Check(fromChange, Equals, "99") 267 c.Check(name, Equals, whatRefreshes) 268 c.Check(opts.Channel, Equals, "18") 269 270 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s to track %s", name, opts.Channel)) 271 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 272 tValidate.WaitFor(tDownload) 273 tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) 274 tUpdate.WaitFor(tValidate) 275 ts := state.NewTaskSet(tDownload, tValidate, tUpdate) 276 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 277 return ts, nil 278 }) 279 defer restore() 280 281 // set a model assertion 282 current := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 283 "architecture": "amd64", 284 "kernel": "pc-kernel", 285 "gadget": "pc", 286 "base": "core18", 287 }) 288 err := assertstate.Add(s.state, current) 289 c.Assert(err, IsNil) 290 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 291 Brand: "canonical", 292 Model: "pc-model", 293 }) 294 295 headers := map[string]interface{}{ 296 "architecture": "amd64", 297 "kernel": "pc-kernel", 298 "gadget": "pc", 299 "base": "core18", 300 "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"}, 301 "revision": "1", 302 } 303 for k, v := range newModelOverrides { 304 headers[k] = v 305 } 306 new := s.brands.Model("canonical", "pc-model", headers) 307 308 testDeviceCtx = &snapstatetest.TrivialDeviceContext{Remodeling: true} 309 310 tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99") 311 c.Assert(err, IsNil) 312 // 2 snaps, plus one track switch plus the remodel task, the 313 // wait chain is tested in TestRemodel* 314 c.Assert(tss, HasLen, 4) 315 } 316 317 func (s *deviceMgrRemodelSuite) TestRemodelTasksSwitchGadget(c *C) { 318 s.testRemodelSwitchTasks(c, "other-gadget", "18", map[string]interface{}{ 319 "gadget": "other-gadget=18", 320 }) 321 } 322 323 func (s *deviceMgrRemodelSuite) TestRemodelTasksSwitchKernel(c *C) { 324 s.testRemodelSwitchTasks(c, "other-kernel", "18", map[string]interface{}{ 325 "kernel": "other-kernel=18", 326 }) 327 } 328 329 func (s *deviceMgrRemodelSuite) testRemodelSwitchTasks(c *C, whatsNew, whatNewTrack string, newModelOverrides map[string]interface{}) { 330 c.Check(newModelOverrides, HasLen, 1, Commentf("test expects a single model property to change")) 331 s.state.Lock() 332 defer s.state.Unlock() 333 s.state.Set("seeded", true) 334 s.state.Set("refresh-privacy-key", "some-privacy-key") 335 336 var testDeviceCtx snapstate.DeviceContext 337 338 var snapstateInstallWithDeviceContextCalled int 339 restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { 340 snapstateInstallWithDeviceContextCalled++ 341 c.Check(name, Equals, whatsNew) 342 if whatNewTrack != "" { 343 c.Check(opts.Channel, Equals, whatNewTrack) 344 } 345 346 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 347 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 348 tValidate.WaitFor(tDownload) 349 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 350 tInstall.WaitFor(tValidate) 351 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 352 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 353 return ts, nil 354 }) 355 defer restore() 356 357 // set a model assertion 358 current := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 359 "architecture": "amd64", 360 "kernel": "pc-kernel", 361 "gadget": "pc", 362 "base": "core18", 363 }) 364 err := assertstate.Add(s.state, current) 365 c.Assert(err, IsNil) 366 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 367 Brand: "canonical", 368 Model: "pc-model", 369 }) 370 371 headers := map[string]interface{}{ 372 "architecture": "amd64", 373 "kernel": "pc-kernel", 374 "gadget": "pc", 375 "base": "core18", 376 "revision": "1", 377 } 378 for k, v := range newModelOverrides { 379 headers[k] = v 380 } 381 new := s.brands.Model("canonical", "pc-model", headers) 382 383 testDeviceCtx = &snapstatetest.TrivialDeviceContext{Remodeling: true} 384 385 tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99") 386 c.Assert(err, IsNil) 387 // 1 of switch-kernel/base/gadget plus the remodel task 388 c.Assert(tss, HasLen, 2) 389 // API was hit 390 c.Assert(snapstateInstallWithDeviceContextCalled, Equals, 1) 391 } 392 393 func (s *deviceMgrRemodelSuite) TestRemodelRequiredSnaps(c *C) { 394 s.state.Lock() 395 defer s.state.Unlock() 396 s.state.Set("seeded", true) 397 s.state.Set("refresh-privacy-key", "some-privacy-key") 398 399 restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { 400 c.Check(flags.Required, Equals, true) 401 c.Check(deviceCtx, NotNil) 402 c.Check(deviceCtx.ForRemodeling(), Equals, true) 403 404 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 405 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 406 tValidate.WaitFor(tDownload) 407 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 408 tInstall.WaitFor(tValidate) 409 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 410 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 411 return ts, nil 412 }) 413 defer restore() 414 415 // set a model assertion 416 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 417 "architecture": "amd64", 418 "kernel": "pc-kernel", 419 "gadget": "pc", 420 "base": "core18", 421 }) 422 s.makeSerialAssertionInState(c, "canonical", "pc-model", "1234") 423 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 424 Brand: "canonical", 425 Model: "pc-model", 426 Serial: "1234", 427 }) 428 429 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 430 "architecture": "amd64", 431 "kernel": "pc-kernel", 432 "gadget": "pc", 433 "base": "core18", 434 "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"}, 435 "revision": "1", 436 }) 437 chg, err := devicestate.Remodel(s.state, new) 438 c.Assert(err, IsNil) 439 c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") 440 441 tl := chg.Tasks() 442 // 2 snaps, 443 c.Assert(tl, HasLen, 2*3+1) 444 445 deviceCtx, err := devicestate.DeviceCtx(s.state, tl[0], nil) 446 c.Assert(err, IsNil) 447 // deviceCtx is actually a remodelContext here 448 remodCtx, ok := deviceCtx.(devicestate.RemodelContext) 449 c.Assert(ok, Equals, true) 450 c.Check(remodCtx.ForRemodeling(), Equals, true) 451 c.Check(remodCtx.Kind(), Equals, devicestate.UpdateRemodel) 452 c.Check(remodCtx.Model(), DeepEquals, new) 453 c.Check(remodCtx.Store(), IsNil) 454 455 // check the tasks 456 tDownloadSnap1 := tl[0] 457 tValidateSnap1 := tl[1] 458 tInstallSnap1 := tl[2] 459 tDownloadSnap2 := tl[3] 460 tValidateSnap2 := tl[4] 461 tInstallSnap2 := tl[5] 462 tSetModel := tl[6] 463 464 // check the tasks 465 c.Assert(tDownloadSnap1.Kind(), Equals, "fake-download") 466 c.Assert(tDownloadSnap1.Summary(), Equals, "Download new-required-snap-1") 467 c.Assert(tDownloadSnap1.WaitTasks(), HasLen, 0) 468 c.Assert(tValidateSnap1.Kind(), Equals, "validate-snap") 469 c.Assert(tValidateSnap1.Summary(), Equals, "Validate new-required-snap-1") 470 c.Assert(tDownloadSnap1.WaitTasks(), HasLen, 0) 471 c.Assert(tDownloadSnap2.Kind(), Equals, "fake-download") 472 c.Assert(tDownloadSnap2.Summary(), Equals, "Download new-required-snap-2") 473 // check the ordering, download/validate everything first, then install 474 475 // snap2 downloads wait for the downloads of snap1 476 c.Assert(tDownloadSnap1.WaitTasks(), HasLen, 0) 477 c.Assert(tValidateSnap1.WaitTasks(), DeepEquals, []*state.Task{ 478 tDownloadSnap1, 479 }) 480 c.Assert(tDownloadSnap2.WaitTasks(), DeepEquals, []*state.Task{ 481 tValidateSnap1, 482 }) 483 c.Assert(tValidateSnap2.WaitTasks(), DeepEquals, []*state.Task{ 484 tDownloadSnap2, 485 }) 486 c.Assert(tInstallSnap1.WaitTasks(), DeepEquals, []*state.Task{ 487 // wait for own check-snap 488 tValidateSnap1, 489 // and also the last check-snap of the download chain 490 tValidateSnap2, 491 }) 492 c.Assert(tInstallSnap2.WaitTasks(), DeepEquals, []*state.Task{ 493 // last snap of the download chain 494 tValidateSnap2, 495 // previous install chain 496 tInstallSnap1, 497 }) 498 499 c.Assert(tSetModel.Kind(), Equals, "set-model") 500 c.Assert(tSetModel.Summary(), Equals, "Set new model assertion") 501 // setModel waits for everything in the change 502 c.Assert(tSetModel.WaitTasks(), DeepEquals, []*state.Task{tDownloadSnap1, tValidateSnap1, tInstallSnap1, tDownloadSnap2, tValidateSnap2, tInstallSnap2}) 503 } 504 505 func (s *deviceMgrRemodelSuite) TestRemodelSwitchKernelTrack(c *C) { 506 s.state.Lock() 507 defer s.state.Unlock() 508 s.state.Set("seeded", true) 509 s.state.Set("refresh-privacy-key", "some-privacy-key") 510 511 restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { 512 c.Check(flags.Required, Equals, true) 513 c.Check(deviceCtx, NotNil) 514 c.Check(deviceCtx.ForRemodeling(), Equals, true) 515 516 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 517 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 518 tValidate.WaitFor(tDownload) 519 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 520 tInstall.WaitFor(tValidate) 521 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 522 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 523 return ts, nil 524 }) 525 defer restore() 526 527 restore = devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { 528 c.Check(flags.Required, Equals, false) 529 c.Check(flags.NoReRefresh, Equals, true) 530 c.Check(deviceCtx, NotNil) 531 c.Check(deviceCtx.ForRemodeling(), Equals, true) 532 533 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s to track %s", name, opts.Channel)) 534 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 535 tValidate.WaitFor(tDownload) 536 tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) 537 tUpdate.WaitFor(tValidate) 538 ts := state.NewTaskSet(tDownload, tValidate, tUpdate) 539 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 540 return ts, nil 541 }) 542 defer restore() 543 544 // set a model assertion 545 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 546 "architecture": "amd64", 547 "kernel": "pc-kernel", 548 "gadget": "pc", 549 "base": "core18", 550 }) 551 s.makeSerialAssertionInState(c, "canonical", "pc-model", "1234") 552 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 553 Brand: "canonical", 554 Model: "pc-model", 555 Serial: "1234", 556 }) 557 558 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 559 "architecture": "amd64", 560 "kernel": "pc-kernel=18", 561 "gadget": "pc", 562 "base": "core18", 563 "required-snaps": []interface{}{"new-required-snap-1"}, 564 "revision": "1", 565 }) 566 chg, err := devicestate.Remodel(s.state, new) 567 c.Assert(err, IsNil) 568 c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") 569 570 tl := chg.Tasks() 571 c.Assert(tl, HasLen, 2*3+1) 572 573 tDownloadKernel := tl[0] 574 tValidateKernel := tl[1] 575 tUpdateKernel := tl[2] 576 tDownloadSnap1 := tl[3] 577 tValidateSnap1 := tl[4] 578 tInstallSnap1 := tl[5] 579 tSetModel := tl[6] 580 581 c.Assert(tDownloadKernel.Kind(), Equals, "fake-download") 582 c.Assert(tDownloadKernel.Summary(), Equals, "Download pc-kernel to track 18") 583 c.Assert(tValidateKernel.Kind(), Equals, "validate-snap") 584 c.Assert(tValidateKernel.Summary(), Equals, "Validate pc-kernel") 585 c.Assert(tUpdateKernel.Kind(), Equals, "fake-update") 586 c.Assert(tUpdateKernel.Summary(), Equals, "Update pc-kernel to track 18") 587 c.Assert(tDownloadSnap1.Kind(), Equals, "fake-download") 588 c.Assert(tDownloadSnap1.Summary(), Equals, "Download new-required-snap-1") 589 c.Assert(tValidateSnap1.Kind(), Equals, "validate-snap") 590 c.Assert(tValidateSnap1.Summary(), Equals, "Validate new-required-snap-1") 591 c.Assert(tInstallSnap1.Kind(), Equals, "fake-install") 592 c.Assert(tInstallSnap1.Summary(), Equals, "Install new-required-snap-1") 593 594 c.Assert(tSetModel.Kind(), Equals, "set-model") 595 c.Assert(tSetModel.Summary(), Equals, "Set new model assertion") 596 597 // check the ordering 598 c.Assert(tDownloadSnap1.WaitTasks(), DeepEquals, []*state.Task{ 599 // previous download finished 600 tValidateKernel, 601 }) 602 c.Assert(tInstallSnap1.WaitTasks(), DeepEquals, []*state.Task{ 603 // last download in the chain finished 604 tValidateSnap1, 605 // and kernel got updated 606 tUpdateKernel, 607 }) 608 c.Assert(tUpdateKernel.WaitTasks(), DeepEquals, []*state.Task{ 609 // kernel is valid 610 tValidateKernel, 611 // and last download in the chain finished 612 tValidateSnap1, 613 }) 614 } 615 616 func (s *deviceMgrRemodelSuite) TestRemodelLessRequiredSnaps(c *C) { 617 s.state.Lock() 618 defer s.state.Unlock() 619 s.state.Set("seeded", true) 620 s.state.Set("refresh-privacy-key", "some-privacy-key") 621 622 // set a model assertion 623 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 624 "architecture": "amd64", 625 "kernel": "pc-kernel", 626 "gadget": "pc", 627 "base": "core18", 628 "required-snaps": []interface{}{"some-required-snap"}, 629 }) 630 s.makeSerialAssertionInState(c, "canonical", "pc-model", "1234") 631 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 632 Brand: "canonical", 633 Model: "pc-model", 634 Serial: "1234", 635 }) 636 637 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 638 "architecture": "amd64", 639 "kernel": "pc-kernel", 640 "gadget": "pc", 641 "base": "core18", 642 "revision": "1", 643 }) 644 chg, err := devicestate.Remodel(s.state, new) 645 c.Assert(err, IsNil) 646 c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") 647 648 tl := chg.Tasks() 649 c.Assert(tl, HasLen, 1) 650 tSetModel := tl[0] 651 c.Assert(tSetModel.Kind(), Equals, "set-model") 652 c.Assert(tSetModel.Summary(), Equals, "Set new model assertion") 653 } 654 655 type freshSessionStore struct { 656 storetest.Store 657 658 ensureDeviceSession int 659 } 660 661 func (sto *freshSessionStore) EnsureDeviceSession() (*auth.DeviceState, error) { 662 sto.ensureDeviceSession += 1 663 return nil, nil 664 } 665 666 func (s *deviceMgrRemodelSuite) TestRemodelStoreSwitch(c *C) { 667 s.state.Lock() 668 defer s.state.Unlock() 669 s.state.Set("seeded", true) 670 s.state.Set("refresh-privacy-key", "some-privacy-key") 671 672 var testStore snapstate.StoreService 673 674 restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { 675 c.Check(flags.Required, Equals, true) 676 c.Check(deviceCtx, NotNil) 677 c.Check(deviceCtx.ForRemodeling(), Equals, true) 678 679 c.Check(deviceCtx.Store(), Equals, testStore) 680 681 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 682 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 683 tValidate.WaitFor(tDownload) 684 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 685 tInstall.WaitFor(tValidate) 686 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 687 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 688 return ts, nil 689 }) 690 defer restore() 691 692 // set a model assertion 693 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 694 "architecture": "amd64", 695 "kernel": "pc-kernel", 696 "gadget": "pc", 697 "base": "core18", 698 }) 699 s.makeSerialAssertionInState(c, "canonical", "pc-model", "1234") 700 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 701 Brand: "canonical", 702 Model: "pc-model", 703 Serial: "1234", 704 }) 705 706 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 707 "architecture": "amd64", 708 "kernel": "pc-kernel", 709 "gadget": "pc", 710 "base": "core18", 711 "store": "switched-store", 712 "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"}, 713 "revision": "1", 714 }) 715 716 freshStore := &freshSessionStore{} 717 testStore = freshStore 718 719 s.newFakeStore = func(devBE storecontext.DeviceBackend) snapstate.StoreService { 720 mod, err := devBE.Model() 721 c.Check(err, IsNil) 722 if err == nil { 723 c.Check(mod, DeepEquals, new) 724 } 725 return testStore 726 } 727 728 chg, err := devicestate.Remodel(s.state, new) 729 c.Assert(err, IsNil) 730 c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") 731 732 c.Check(freshStore.ensureDeviceSession, Equals, 1) 733 734 tl := chg.Tasks() 735 // 2 snaps * 3 tasks (from the mock install above) + 736 // 1 "set-model" task at the end 737 c.Assert(tl, HasLen, 2*3+1) 738 739 deviceCtx, err := devicestate.DeviceCtx(s.state, tl[0], nil) 740 c.Assert(err, IsNil) 741 // deviceCtx is actually a remodelContext here 742 remodCtx, ok := deviceCtx.(devicestate.RemodelContext) 743 c.Assert(ok, Equals, true) 744 c.Check(remodCtx.ForRemodeling(), Equals, true) 745 c.Check(remodCtx.Kind(), Equals, devicestate.StoreSwitchRemodel) 746 c.Check(remodCtx.Model(), DeepEquals, new) 747 c.Check(remodCtx.Store(), Equals, testStore) 748 } 749 750 func (s *deviceMgrRemodelSuite) TestRemodelRereg(c *C) { 751 s.state.Lock() 752 defer s.state.Unlock() 753 s.state.Set("seeded", true) 754 755 // set a model assertion 756 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 757 "architecture": "amd64", 758 "kernel": "pc-kernel", 759 "gadget": "pc", 760 "base": "core18", 761 }) 762 s.makeSerialAssertionInState(c, "canonical", "pc-model", "orig-serial") 763 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 764 Brand: "canonical", 765 Model: "pc-model", 766 Serial: "orig-serial", 767 SessionMacaroon: "old-session", 768 }) 769 770 new := s.brands.Model("canonical", "rereg-model", map[string]interface{}{ 771 "architecture": "amd64", 772 "kernel": "pc-kernel", 773 "gadget": "pc", 774 "base": "core18", 775 "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"}, 776 }) 777 778 s.newFakeStore = func(devBE storecontext.DeviceBackend) snapstate.StoreService { 779 mod, err := devBE.Model() 780 c.Check(err, IsNil) 781 if err == nil { 782 c.Check(mod, DeepEquals, new) 783 } 784 return nil 785 } 786 787 chg, err := devicestate.Remodel(s.state, new) 788 c.Assert(err, IsNil) 789 790 c.Assert(chg.Summary(), Equals, "Remodel device to canonical/rereg-model (0)") 791 792 tl := chg.Tasks() 793 c.Assert(tl, HasLen, 2) 794 795 // check the tasks 796 tRequestSerial := tl[0] 797 tPrepareRemodeling := tl[1] 798 799 // check the tasks 800 c.Assert(tRequestSerial.Kind(), Equals, "request-serial") 801 c.Assert(tRequestSerial.Summary(), Equals, "Request new device serial") 802 c.Assert(tRequestSerial.WaitTasks(), HasLen, 0) 803 804 c.Assert(tPrepareRemodeling.Kind(), Equals, "prepare-remodeling") 805 c.Assert(tPrepareRemodeling.Summary(), Equals, "Prepare remodeling") 806 c.Assert(tPrepareRemodeling.WaitTasks(), DeepEquals, []*state.Task{tRequestSerial}) 807 } 808 809 func (s *deviceMgrRemodelSuite) TestRemodelClash(c *C) { 810 s.state.Lock() 811 defer s.state.Unlock() 812 s.state.Set("seeded", true) 813 s.state.Set("refresh-privacy-key", "some-privacy-key") 814 815 var clashing *asserts.Model 816 817 restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { 818 // simulate things changing under our feet 819 assertstatetest.AddMany(st, clashing) 820 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 821 Brand: "canonical", 822 Model: clashing.Model(), 823 }) 824 825 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 826 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 827 tValidate.WaitFor(tDownload) 828 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 829 tInstall.WaitFor(tValidate) 830 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 831 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 832 return ts, nil 833 }) 834 defer restore() 835 836 // set a model assertion 837 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 838 "architecture": "amd64", 839 "kernel": "pc-kernel", 840 "gadget": "pc", 841 "base": "core18", 842 }) 843 s.makeSerialAssertionInState(c, "canonical", "pc-model", "1234") 844 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 845 Brand: "canonical", 846 Model: "pc-model", 847 Serial: "1234", 848 }) 849 850 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 851 "architecture": "amd64", 852 "kernel": "pc-kernel", 853 "gadget": "pc", 854 "base": "core18", 855 "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"}, 856 "revision": "1", 857 }) 858 other := s.brands.Model("canonical", "pc-model-other", map[string]interface{}{ 859 "architecture": "amd64", 860 "kernel": "pc-kernel", 861 "gadget": "pc", 862 "base": "core18", 863 "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"}, 864 }) 865 866 clashing = other 867 _, err := devicestate.Remodel(s.state, new) 868 c.Check(err, DeepEquals, &snapstate.ChangeConflictError{ 869 Message: "cannot start remodel, clashing with concurrent remodel to canonical/pc-model-other (0)", 870 }) 871 872 // reset 873 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 874 Brand: "canonical", 875 Model: "pc-model", 876 Serial: "1234", 877 }) 878 clashing = new 879 _, err = devicestate.Remodel(s.state, new) 880 c.Check(err, DeepEquals, &snapstate.ChangeConflictError{ 881 Message: "cannot start remodel, clashing with concurrent remodel to canonical/pc-model (1)", 882 }) 883 } 884 885 func (s *deviceMgrRemodelSuite) TestRemodelClashInProgress(c *C) { 886 s.state.Lock() 887 defer s.state.Unlock() 888 s.state.Set("seeded", true) 889 s.state.Set("refresh-privacy-key", "some-privacy-key") 890 891 restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { 892 // simulate another started remodeling 893 st.NewChange("remodel", "other remodel") 894 895 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 896 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 897 tValidate.WaitFor(tDownload) 898 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 899 tInstall.WaitFor(tValidate) 900 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 901 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 902 return ts, nil 903 }) 904 defer restore() 905 906 // set a model assertion 907 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 908 "architecture": "amd64", 909 "kernel": "pc-kernel", 910 "gadget": "pc", 911 "base": "core18", 912 }) 913 s.makeSerialAssertionInState(c, "canonical", "pc-model", "1234") 914 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 915 Brand: "canonical", 916 Model: "pc-model", 917 Serial: "1234", 918 }) 919 920 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 921 "architecture": "amd64", 922 "kernel": "pc-kernel", 923 "gadget": "pc", 924 "base": "core18", 925 "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"}, 926 "revision": "1", 927 }) 928 929 _, err := devicestate.Remodel(s.state, new) 930 c.Check(err, DeepEquals, &snapstate.ChangeConflictError{ 931 Message: "cannot start remodel, clashing with concurrent one", 932 }) 933 } 934 935 func (s *deviceMgrRemodelSuite) TestReregRemodelClashAnyChange(c *C) { 936 s.state.Lock() 937 defer s.state.Unlock() 938 s.state.Set("seeded", true) 939 940 // set a model assertion 941 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 942 "architecture": "amd64", 943 "kernel": "pc-kernel", 944 "gadget": "pc", 945 "base": "core18", 946 }) 947 s.makeSerialAssertionInState(c, "canonical", "pc-model", "orig-serial") 948 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 949 Brand: "canonical", 950 Model: "pc-model", 951 Serial: "orig-serial", 952 SessionMacaroon: "old-session", 953 }) 954 955 new := s.brands.Model("canonical", "pc-model-2", map[string]interface{}{ 956 "architecture": "amd64", 957 "kernel": "pc-kernel", 958 "gadget": "pc", 959 "base": "core18", 960 "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"}, 961 "revision": "1", 962 }) 963 s.newFakeStore = func(devBE storecontext.DeviceBackend) snapstate.StoreService { 964 // we never reach the place where this gets called 965 c.Fatalf("unexpected call") 966 return nil 967 } 968 969 // simulate any other change 970 chg := s.state.NewChange("chg", "other change") 971 chg.SetStatus(state.DoingStatus) 972 973 _, err := devicestate.Remodel(s.state, new) 974 c.Assert(err, NotNil) 975 c.Assert(err, DeepEquals, &snapstate.ChangeConflictError{ 976 ChangeKind: "chg", 977 Message: `other changes in progress (conflicting change "chg"), change "remodel" not allowed until they are done`, 978 }) 979 } 980 981 func (s *deviceMgrRemodelSuite) TestRemodeling(c *C) { 982 s.state.Lock() 983 defer s.state.Unlock() 984 985 // no changes 986 c.Check(devicestate.Remodeling(s.state), Equals, false) 987 988 // other change 989 s.state.NewChange("other", "...") 990 c.Check(devicestate.Remodeling(s.state), Equals, false) 991 992 // remodel change 993 chg := s.state.NewChange("remodel", "...") 994 c.Check(devicestate.Remodeling(s.state), Equals, true) 995 996 // done 997 chg.SetStatus(state.DoneStatus) 998 c.Check(devicestate.Remodeling(s.state), Equals, false) 999 } 1000 1001 func (s *deviceMgrRemodelSuite) TestDeviceCtxNoTask(c *C) { 1002 s.state.Lock() 1003 defer s.state.Unlock() 1004 // nothing in the state 1005 1006 _, err := devicestate.DeviceCtx(s.state, nil, nil) 1007 c.Check(err, Equals, state.ErrNoState) 1008 1009 // have a model assertion 1010 model := s.brands.Model("canonical", "pc", map[string]interface{}{ 1011 "gadget": "pc", 1012 "kernel": "kernel", 1013 "architecture": "amd64", 1014 }) 1015 assertstatetest.AddMany(s.state, model) 1016 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 1017 Brand: "canonical", 1018 Model: "pc", 1019 }) 1020 1021 deviceCtx, err := devicestate.DeviceCtx(s.state, nil, nil) 1022 c.Assert(err, IsNil) 1023 c.Assert(deviceCtx.Model().BrandID(), Equals, "canonical") 1024 1025 c.Check(deviceCtx.Classic(), Equals, false) 1026 c.Check(deviceCtx.Kernel(), Equals, "kernel") 1027 c.Check(deviceCtx.Base(), Equals, "") 1028 c.Check(deviceCtx.RunMode(), Equals, true) 1029 // not a uc20 model, so no modeenv 1030 c.Check(deviceCtx.HasModeenv(), Equals, false) 1031 } 1032 1033 func (s *deviceMgrRemodelSuite) TestDeviceCtxGroundContext(c *C) { 1034 s.state.Lock() 1035 defer s.state.Unlock() 1036 1037 // have a model assertion 1038 model := s.brands.Model("canonical", "pc", map[string]interface{}{ 1039 "gadget": "pc", 1040 "kernel": "kernel", 1041 "architecture": "amd64", 1042 }) 1043 assertstatetest.AddMany(s.state, model) 1044 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 1045 Brand: "canonical", 1046 Model: "pc", 1047 }) 1048 1049 deviceCtx, err := devicestate.DeviceCtx(s.state, nil, nil) 1050 c.Assert(err, IsNil) 1051 c.Assert(deviceCtx.Model().BrandID(), Equals, "canonical") 1052 groundCtx := deviceCtx.GroundContext() 1053 c.Check(groundCtx.ForRemodeling(), Equals, false) 1054 c.Check(groundCtx.Model().Model(), Equals, "pc") 1055 c.Check(groundCtx.Store, PanicMatches, `retrieved ground context is not intended to drive store operations`) 1056 } 1057 1058 func (s *deviceMgrRemodelSuite) TestDeviceCtxProvided(c *C) { 1059 s.state.Lock() 1060 defer s.state.Unlock() 1061 1062 model := assertstest.FakeAssertion(map[string]interface{}{ 1063 "type": "model", 1064 "authority-id": "canonical", 1065 "series": "16", 1066 "brand-id": "canonical", 1067 "model": "pc", 1068 "gadget": "pc", 1069 "kernel": "kernel", 1070 "architecture": "amd64", 1071 }).(*asserts.Model) 1072 1073 deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: model} 1074 1075 deviceCtx1, err := devicestate.DeviceCtx(s.state, nil, deviceCtx) 1076 c.Assert(err, IsNil) 1077 c.Assert(deviceCtx1, Equals, deviceCtx) 1078 } 1079 1080 func (s *deviceMgrRemodelSuite) TestCheckGadgetRemodelCompatible(c *C) { 1081 s.state.Lock() 1082 defer s.state.Unlock() 1083 1084 currentSnapYaml := ` 1085 name: gadget 1086 type: gadget 1087 version: 123 1088 ` 1089 remodelSnapYaml := ` 1090 name: new-gadget 1091 type: gadget 1092 version: 123 1093 ` 1094 mockGadget := ` 1095 type: gadget 1096 name: gadget 1097 volumes: 1098 volume: 1099 schema: gpt 1100 bootloader: grub 1101 ` 1102 siCurrent := &snap.SideInfo{Revision: snap.R(123), RealName: "gadget"} 1103 // so that we get a directory 1104 currInfo := snaptest.MockSnapWithFiles(c, currentSnapYaml, siCurrent, nil) 1105 info := snaptest.MockSnapWithFiles(c, remodelSnapYaml, &snap.SideInfo{Revision: snap.R(1)}, nil) 1106 snapf, err := snapfile.Open(info.MountDir()) 1107 c.Assert(err, IsNil) 1108 1109 s.setupBrands(c) 1110 1111 oldModel := fakeMyModel(map[string]interface{}{ 1112 "architecture": "amd64", 1113 "gadget": "gadget", 1114 "kernel": "kernel", 1115 }) 1116 deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: oldModel} 1117 1118 // model assertion in device context 1119 newModel := fakeMyModel(map[string]interface{}{ 1120 "architecture": "amd64", 1121 "gadget": "new-gadget", 1122 "kernel": "kernel", 1123 }) 1124 remodelCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: newModel, Remodeling: true, OldDeviceModel: oldModel} 1125 1126 restore := devicestate.MockGadgetIsCompatible(func(current, update *gadget.Info) error { 1127 c.Assert(current.Volumes, HasLen, 1) 1128 c.Assert(update.Volumes, HasLen, 1) 1129 return errors.New("fail") 1130 }) 1131 defer restore() 1132 1133 // not on classic 1134 release.OnClassic = true 1135 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, currInfo, snapf, snapstate.Flags{}, remodelCtx) 1136 c.Check(err, IsNil) 1137 release.OnClassic = false 1138 1139 // nothing if not remodeling 1140 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, currInfo, snapf, snapstate.Flags{}, deviceCtx) 1141 c.Check(err, IsNil) 1142 1143 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, currInfo, snapf, snapstate.Flags{}, remodelCtx) 1144 c.Check(err, ErrorMatches, "cannot read new gadget metadata: .*/new-gadget/1/meta/gadget.yaml: no such file or directory") 1145 1146 // drop gadget.yaml to the new gadget 1147 err = ioutil.WriteFile(filepath.Join(info.MountDir(), "meta/gadget.yaml"), []byte(mockGadget), 0644) 1148 c.Assert(err, IsNil) 1149 1150 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, currInfo, snapf, snapstate.Flags{}, remodelCtx) 1151 c.Check(err, ErrorMatches, "cannot read current gadget metadata: .*/gadget/123/meta/gadget.yaml: no such file or directory") 1152 1153 // drop gadget.yaml to the current gadget 1154 err = ioutil.WriteFile(filepath.Join(currInfo.MountDir(), "meta/gadget.yaml"), []byte(mockGadget), 0644) 1155 c.Assert(err, IsNil) 1156 1157 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, currInfo, snapf, snapstate.Flags{}, remodelCtx) 1158 c.Check(err, ErrorMatches, "cannot remodel to an incompatible gadget: fail") 1159 1160 restore = devicestate.MockGadgetIsCompatible(func(current, update *gadget.Info) error { 1161 c.Assert(current.Volumes, HasLen, 1) 1162 c.Assert(update.Volumes, HasLen, 1) 1163 return nil 1164 }) 1165 defer restore() 1166 1167 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, currInfo, snapf, snapstate.Flags{}, remodelCtx) 1168 c.Check(err, IsNil) 1169 1170 // when remodeling to completely new gadget snap, there is no current 1171 // snap passed to the check callback 1172 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, nil, snapf, snapstate.Flags{}, remodelCtx) 1173 c.Check(err, ErrorMatches, "cannot identify the current gadget snap") 1174 1175 // mock data to obtain current gadget info 1176 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 1177 Brand: "canonical", 1178 Model: "gadget", 1179 }) 1180 s.makeModelAssertionInState(c, "canonical", "gadget", map[string]interface{}{ 1181 "architecture": "amd64", 1182 "kernel": "kernel", 1183 "gadget": "gadget", 1184 }) 1185 1186 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, nil, snapf, snapstate.Flags{}, remodelCtx) 1187 c.Check(err, ErrorMatches, "cannot identify the current gadget snap") 1188 1189 snapstate.Set(s.state, "gadget", &snapstate.SnapState{ 1190 SnapType: "gadget", 1191 Sequence: []*snap.SideInfo{siCurrent}, 1192 Current: siCurrent.Revision, 1193 Active: true, 1194 }) 1195 1196 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, nil, snapf, snapstate.Flags{}, remodelCtx) 1197 c.Check(err, IsNil) 1198 } 1199 1200 var ( 1201 compatibleTestMockOkGadget = ` 1202 type: gadget 1203 name: gadget 1204 volumes: 1205 volume: 1206 schema: gpt 1207 bootloader: grub 1208 structure: 1209 - name: foo 1210 size: 10M 1211 type: 00000000-0000-0000-0000-0000deadbeef 1212 ` 1213 ) 1214 1215 func (s *deviceMgrRemodelSuite) testCheckGadgetRemodelCompatibleWithYaml(c *C, currentGadgetYaml, newGadgetYaml string, expErr string) { 1216 s.state.Lock() 1217 defer s.state.Unlock() 1218 1219 currentSnapYaml := ` 1220 name: gadget 1221 type: gadget 1222 version: 123 1223 ` 1224 remodelSnapYaml := ` 1225 name: new-gadget 1226 type: gadget 1227 version: 123 1228 ` 1229 1230 currInfo := snaptest.MockSnapWithFiles(c, currentSnapYaml, &snap.SideInfo{Revision: snap.R(123)}, [][]string{ 1231 {"meta/gadget.yaml", currentGadgetYaml}, 1232 }) 1233 // gadget we're remodeling to is identical 1234 info := snaptest.MockSnapWithFiles(c, remodelSnapYaml, &snap.SideInfo{Revision: snap.R(1)}, [][]string{ 1235 {"meta/gadget.yaml", newGadgetYaml}, 1236 }) 1237 snapf, err := snapfile.Open(info.MountDir()) 1238 c.Assert(err, IsNil) 1239 1240 s.setupBrands(c) 1241 // model assertion in device context 1242 oldModel := fakeMyModel(map[string]interface{}{ 1243 "architecture": "amd64", 1244 "gadget": "new-gadget", 1245 "kernel": "krnl", 1246 }) 1247 model := fakeMyModel(map[string]interface{}{ 1248 "architecture": "amd64", 1249 "gadget": "new-gadget", 1250 "kernel": "krnl", 1251 }) 1252 remodelCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: model, Remodeling: true, OldDeviceModel: oldModel} 1253 1254 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, currInfo, snapf, snapstate.Flags{}, remodelCtx) 1255 if expErr == "" { 1256 c.Check(err, IsNil) 1257 } else { 1258 c.Check(err, ErrorMatches, expErr) 1259 } 1260 1261 } 1262 1263 func (s *deviceMgrRemodelSuite) TestCheckGadgetRemodelCompatibleWithYamlHappy(c *C) { 1264 s.testCheckGadgetRemodelCompatibleWithYaml(c, compatibleTestMockOkGadget, compatibleTestMockOkGadget, "") 1265 } 1266 1267 func (s *deviceMgrRemodelSuite) TestCheckGadgetRemodelCompatibleWithYamlBad(c *C) { 1268 mockBadGadgetYaml := ` 1269 type: gadget 1270 name: gadget 1271 volumes: 1272 volume: 1273 schema: gpt 1274 bootloader: grub 1275 structure: 1276 - name: foo 1277 size: 20M 1278 type: 00000000-0000-0000-0000-0000deadbeef 1279 ` 1280 1281 errMatch := `cannot remodel to an incompatible gadget: incompatible layout change: incompatible structure #0 \("foo"\) change: cannot change structure size from 10485760 to 20971520` 1282 s.testCheckGadgetRemodelCompatibleWithYaml(c, compatibleTestMockOkGadget, mockBadGadgetYaml, errMatch) 1283 } 1284 1285 func (s *deviceMgrRemodelSuite) TestRemodelGadgetAssetsUpdate(c *C) { 1286 var currentGadgetYaml = ` 1287 volumes: 1288 pc: 1289 bootloader: grub 1290 structure: 1291 - name: foo 1292 type: 00000000-0000-0000-0000-0000deadcafe 1293 filesystem: ext4 1294 size: 10M 1295 content: 1296 - source: foo-content 1297 target: / 1298 - name: bare-one 1299 type: bare 1300 size: 1M 1301 content: 1302 - image: bare.img 1303 ` 1304 1305 var remodelGadgetYaml = ` 1306 volumes: 1307 pc: 1308 bootloader: grub 1309 structure: 1310 - name: foo 1311 type: 00000000-0000-0000-0000-0000deadcafe 1312 filesystem: ext4 1313 size: 10M 1314 content: 1315 - source: new-foo-content 1316 target: / 1317 - name: bare-one 1318 type: bare 1319 size: 1M 1320 content: 1321 - image: new-bare-content.img 1322 ` 1323 1324 s.state.Lock() 1325 s.state.Set("seeded", true) 1326 s.state.Set("refresh-privacy-key", "some-privacy-key") 1327 1328 nopHandler := func(task *state.Task, _ *tomb.Tomb) error { 1329 return nil 1330 } 1331 s.o.TaskRunner().AddHandler("fake-download", nopHandler, nil) 1332 s.o.TaskRunner().AddHandler("validate-snap", nopHandler, nil) 1333 s.o.TaskRunner().AddHandler("set-model", nopHandler, nil) 1334 1335 // set a model assertion we remodel from 1336 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 1337 "architecture": "amd64", 1338 "kernel": "pc-kernel", 1339 "gadget": "pc", 1340 "base": "core18", 1341 }) 1342 s.makeSerialAssertionInState(c, "canonical", "pc-model", "serial") 1343 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 1344 Brand: "canonical", 1345 Model: "pc-model", 1346 Serial: "serial", 1347 }) 1348 1349 // the target model 1350 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 1351 "architecture": "amd64", 1352 "kernel": "pc-kernel", 1353 "base": "core18", 1354 "revision": "1", 1355 // remodel to new gadget 1356 "gadget": "new-gadget", 1357 }) 1358 1359 // current gadget 1360 siModelGadget := &snap.SideInfo{ 1361 RealName: "pc", 1362 Revision: snap.R(33), 1363 SnapID: "foo-id", 1364 } 1365 currentGadgetInfo := snaptest.MockSnapWithFiles(c, snapYaml, siModelGadget, [][]string{ 1366 {"meta/gadget.yaml", currentGadgetYaml}, 1367 }) 1368 snapstate.Set(s.state, "pc", &snapstate.SnapState{ 1369 SnapType: "gadget", 1370 Sequence: []*snap.SideInfo{siModelGadget}, 1371 Current: siModelGadget.Revision, 1372 Active: true, 1373 }) 1374 1375 // new gadget snap 1376 siNewModelGadget := &snap.SideInfo{ 1377 RealName: "new-gadget", 1378 Revision: snap.R(34), 1379 } 1380 newGadgetInfo := snaptest.MockSnapWithFiles(c, snapYaml, siNewModelGadget, [][]string{ 1381 {"meta/gadget.yaml", remodelGadgetYaml}, 1382 }) 1383 1384 restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { 1385 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 1386 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 1387 tValidate.WaitFor(tDownload) 1388 tGadgetUpdate := s.state.NewTask("update-gadget-assets", fmt.Sprintf("Update gadget %s", name)) 1389 tGadgetUpdate.Set("snap-setup", &snapstate.SnapSetup{ 1390 SideInfo: siNewModelGadget, 1391 Type: snap.TypeGadget, 1392 }) 1393 tGadgetUpdate.WaitFor(tValidate) 1394 ts := state.NewTaskSet(tDownload, tValidate, tGadgetUpdate) 1395 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 1396 return ts, nil 1397 }) 1398 defer restore() 1399 restore = release.MockOnClassic(false) 1400 defer restore() 1401 1402 gadgetUpdateCalled := false 1403 restore = devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 1404 gadgetUpdateCalled = true 1405 c.Check(policy, NotNil) 1406 c.Check(reflect.ValueOf(policy).Pointer(), Equals, reflect.ValueOf(gadget.RemodelUpdatePolicy).Pointer()) 1407 c.Check(current, DeepEquals, gadget.GadgetData{ 1408 Info: &gadget.Info{ 1409 Volumes: map[string]*gadget.Volume{ 1410 "pc": { 1411 Bootloader: "grub", 1412 Schema: "gpt", 1413 Structure: []gadget.VolumeStructure{{ 1414 Name: "foo", 1415 Type: "00000000-0000-0000-0000-0000deadcafe", 1416 Size: 10 * quantity.SizeMiB, 1417 Filesystem: "ext4", 1418 Content: []gadget.VolumeContent{ 1419 {UnresolvedSource: "foo-content", Target: "/"}, 1420 }, 1421 }, { 1422 Name: "bare-one", 1423 Type: "bare", 1424 Size: quantity.SizeMiB, 1425 Content: []gadget.VolumeContent{ 1426 {Image: "bare.img"}, 1427 }, 1428 }}, 1429 }, 1430 }, 1431 }, 1432 RootDir: currentGadgetInfo.MountDir(), 1433 }) 1434 c.Check(update, DeepEquals, gadget.GadgetData{ 1435 Info: &gadget.Info{ 1436 Volumes: map[string]*gadget.Volume{ 1437 "pc": { 1438 Bootloader: "grub", 1439 Schema: "gpt", 1440 Structure: []gadget.VolumeStructure{{ 1441 Name: "foo", 1442 Type: "00000000-0000-0000-0000-0000deadcafe", 1443 Size: 10 * quantity.SizeMiB, 1444 Filesystem: "ext4", 1445 Content: []gadget.VolumeContent{ 1446 {UnresolvedSource: "new-foo-content", Target: "/"}, 1447 }, 1448 }, { 1449 Name: "bare-one", 1450 Type: "bare", 1451 Size: quantity.SizeMiB, 1452 Content: []gadget.VolumeContent{ 1453 {Image: "new-bare-content.img"}, 1454 }, 1455 }}, 1456 }, 1457 }, 1458 }, 1459 RootDir: newGadgetInfo.MountDir(), 1460 }) 1461 return nil 1462 }) 1463 defer restore() 1464 1465 chg, err := devicestate.Remodel(s.state, new) 1466 c.Assert(err, IsNil) 1467 s.state.Unlock() 1468 1469 s.settle(c) 1470 1471 s.state.Lock() 1472 defer s.state.Unlock() 1473 c.Check(chg.IsReady(), Equals, true) 1474 c.Check(chg.Err(), IsNil) 1475 c.Check(gadgetUpdateCalled, Equals, true) 1476 c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystem}) 1477 } 1478 1479 func (s *deviceMgrRemodelSuite) TestRemodelGadgetAssetsParanoidCheck(c *C) { 1480 s.state.Lock() 1481 s.state.Set("seeded", true) 1482 s.state.Set("refresh-privacy-key", "some-privacy-key") 1483 1484 nopHandler := func(task *state.Task, _ *tomb.Tomb) error { 1485 return nil 1486 } 1487 s.o.TaskRunner().AddHandler("fake-download", nopHandler, nil) 1488 s.o.TaskRunner().AddHandler("validate-snap", nopHandler, nil) 1489 s.o.TaskRunner().AddHandler("set-model", nopHandler, nil) 1490 1491 // set a model assertion we remodel from 1492 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 1493 "architecture": "amd64", 1494 "kernel": "pc-kernel", 1495 "gadget": "pc", 1496 "base": "core18", 1497 }) 1498 s.makeSerialAssertionInState(c, "canonical", "pc-model", "serial") 1499 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 1500 Brand: "canonical", 1501 Model: "pc-model", 1502 Serial: "serial", 1503 }) 1504 1505 // the target model 1506 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 1507 "architecture": "amd64", 1508 "kernel": "pc-kernel", 1509 "base": "core18", 1510 "revision": "1", 1511 // remodel to new gadget 1512 "gadget": "new-gadget", 1513 }) 1514 1515 // current gadget 1516 siModelGadget := &snap.SideInfo{ 1517 RealName: "pc", 1518 Revision: snap.R(33), 1519 SnapID: "foo-id", 1520 } 1521 snapstate.Set(s.state, "pc", &snapstate.SnapState{ 1522 SnapType: "gadget", 1523 Sequence: []*snap.SideInfo{siModelGadget}, 1524 Current: siModelGadget.Revision, 1525 Active: true, 1526 }) 1527 1528 // new gadget snap, name does not match the new model 1529 siUnexpectedModelGadget := &snap.SideInfo{ 1530 RealName: "new-gadget-unexpected", 1531 Revision: snap.R(34), 1532 } 1533 restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { 1534 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 1535 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 1536 tValidate.WaitFor(tDownload) 1537 tGadgetUpdate := s.state.NewTask("update-gadget-assets", fmt.Sprintf("Update gadget %s", name)) 1538 tGadgetUpdate.Set("snap-setup", &snapstate.SnapSetup{ 1539 SideInfo: siUnexpectedModelGadget, 1540 Type: snap.TypeGadget, 1541 }) 1542 tGadgetUpdate.WaitFor(tValidate) 1543 ts := state.NewTaskSet(tDownload, tValidate, tGadgetUpdate) 1544 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 1545 return ts, nil 1546 }) 1547 defer restore() 1548 restore = release.MockOnClassic(false) 1549 defer restore() 1550 1551 gadgetUpdateCalled := false 1552 restore = devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 1553 return errors.New("unexpected call") 1554 }) 1555 defer restore() 1556 1557 chg, err := devicestate.Remodel(s.state, new) 1558 c.Assert(err, IsNil) 1559 s.state.Unlock() 1560 1561 s.settle(c) 1562 1563 s.state.Lock() 1564 defer s.state.Unlock() 1565 c.Check(chg.IsReady(), Equals, true) 1566 c.Assert(chg.Err(), ErrorMatches, `(?s).*\(cannot apply gadget assets update from non-model gadget snap "new-gadget-unexpected", expected "new-gadget" snap\)`) 1567 c.Check(gadgetUpdateCalled, Equals, false) 1568 c.Check(s.restartRequests, HasLen, 0) 1569 } 1570 1571 func (s *deviceMgrSuite) TestRemodelSwitchBase(c *C) { 1572 s.state.Lock() 1573 defer s.state.Unlock() 1574 s.state.Set("seeded", true) 1575 s.state.Set("refresh-privacy-key", "some-privacy-key") 1576 1577 var testDeviceCtx snapstate.DeviceContext 1578 1579 var snapstateInstallWithDeviceContextCalled int 1580 restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { 1581 snapstateInstallWithDeviceContextCalled++ 1582 c.Check(name, Equals, "core20") 1583 1584 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 1585 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 1586 tValidate.WaitFor(tDownload) 1587 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 1588 tInstall.WaitFor(tValidate) 1589 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 1590 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 1591 return ts, nil 1592 }) 1593 defer restore() 1594 1595 // set a model assertion 1596 current := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 1597 "architecture": "amd64", 1598 "kernel": "pc-kernel", 1599 "gadget": "pc", 1600 "base": "core18", 1601 }) 1602 err := assertstate.Add(s.state, current) 1603 c.Assert(err, IsNil) 1604 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 1605 Brand: "canonical", 1606 Model: "pc-model", 1607 }) 1608 1609 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 1610 "architecture": "amd64", 1611 "kernel": "pc-kernel", 1612 "gadget": "pc", 1613 "base": "core20", 1614 "revision": "1", 1615 }) 1616 1617 testDeviceCtx = &snapstatetest.TrivialDeviceContext{Remodeling: true} 1618 1619 tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99") 1620 c.Assert(err, IsNil) 1621 // 1 switch to a new base plus the remodel task 1622 c.Assert(tss, HasLen, 2) 1623 // API was hit 1624 c.Assert(snapstateInstallWithDeviceContextCalled, Equals, 1) 1625 }