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