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