github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/overlord/devicestate/devicestate_remodel_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016-2021 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package devicestate_test 21 22 import ( 23 "context" 24 "errors" 25 "fmt" 26 "io/ioutil" 27 "os" 28 "path/filepath" 29 "reflect" 30 "strings" 31 "time" 32 33 . "gopkg.in/check.v1" 34 "gopkg.in/tomb.v2" 35 36 "github.com/snapcore/snapd/asserts" 37 "github.com/snapcore/snapd/asserts/assertstest" 38 "github.com/snapcore/snapd/boot" 39 "github.com/snapcore/snapd/gadget" 40 "github.com/snapcore/snapd/gadget/quantity" 41 "github.com/snapcore/snapd/logger" 42 "github.com/snapcore/snapd/overlord/assertstate" 43 "github.com/snapcore/snapd/overlord/assertstate/assertstatetest" 44 "github.com/snapcore/snapd/overlord/auth" 45 "github.com/snapcore/snapd/overlord/devicestate" 46 "github.com/snapcore/snapd/overlord/devicestate/devicestatetest" 47 "github.com/snapcore/snapd/overlord/snapstate" 48 "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" 49 "github.com/snapcore/snapd/overlord/state" 50 "github.com/snapcore/snapd/overlord/storecontext" 51 "github.com/snapcore/snapd/release" 52 "github.com/snapcore/snapd/snap" 53 "github.com/snapcore/snapd/snap/snapfile" 54 "github.com/snapcore/snapd/snap/snaptest" 55 "github.com/snapcore/snapd/store/storetest" 56 "github.com/snapcore/snapd/testutil" 57 ) 58 59 type deviceMgrRemodelSuite struct { 60 deviceMgrBaseSuite 61 } 62 63 var _ = Suite(&deviceMgrRemodelSuite{}) 64 65 func (s *deviceMgrRemodelSuite) TestRemodelUnhappyNotSeeded(c *C) { 66 s.state.Lock() 67 defer s.state.Unlock() 68 s.state.Set("seeded", false) 69 70 newModel := s.brands.Model("canonical", "pc", map[string]interface{}{ 71 "architecture": "amd64", 72 "kernel": "pc-kernel", 73 "gadget": "pc", 74 }) 75 _, err := devicestate.Remodel(s.state, newModel) 76 c.Assert(err, ErrorMatches, "cannot remodel until fully seeded") 77 } 78 79 var mockCore20ModelHeaders = map[string]interface{}{ 80 "brand": "canonical", 81 "model": "pc-model-20", 82 "architecture": "amd64", 83 "grade": "dangerous", 84 "base": "core20", 85 "snaps": mockCore20ModelSnaps, 86 } 87 88 var mockCore20ModelSnaps = []interface{}{ 89 map[string]interface{}{ 90 "name": "pc-kernel", 91 "id": "pckernelidididididididididididid", 92 "type": "kernel", 93 "default-channel": "20", 94 }, 95 map[string]interface{}{ 96 "name": "pc", 97 "id": "pcididididididididididididididid", 98 "type": "gadget", 99 "default-channel": "20", 100 }, 101 } 102 103 // copy current model unless new model test data is different 104 // and delete nil keys in new model 105 func mergeMockModelHeaders(cur, new map[string]interface{}) { 106 for k, v := range cur { 107 if v, ok := new[k]; ok { 108 if v == nil { 109 delete(new, k) 110 } 111 continue 112 } 113 new[k] = v 114 } 115 } 116 117 func (s *deviceMgrRemodelSuite) TestRemodelUnhappy(c *C) { 118 s.state.Lock() 119 defer s.state.Unlock() 120 s.state.Set("seeded", true) 121 122 // set a model assertion 123 cur := map[string]interface{}{ 124 "brand": "canonical", 125 "model": "pc-model", 126 "architecture": "amd64", 127 "kernel": "pc-kernel", 128 "gadget": "pc", 129 } 130 s.makeModelAssertionInState(c, cur["brand"].(string), cur["model"].(string), map[string]interface{}{ 131 "architecture": cur["architecture"], 132 "kernel": cur["kernel"], 133 "gadget": cur["gadget"], 134 }) 135 s.makeSerialAssertionInState(c, cur["brand"].(string), cur["model"].(string), "orig-serial") 136 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 137 Brand: cur["brand"].(string), 138 Model: cur["model"].(string), 139 Serial: "orig-serial", 140 }) 141 142 restore := devicestate.AllowUC20RemodelTesting(false) 143 defer restore() 144 // ensure all error cases are checked 145 for _, t := range []struct { 146 new map[string]interface{} 147 errStr string 148 }{ 149 {map[string]interface{}{"architecture": "pdp-7"}, "cannot remodel to different architectures yet"}, 150 {map[string]interface{}{"base": "core18"}, "cannot remodel from core to bases yet"}, 151 {map[string]interface{}{"base": "core20", "kernel": nil, "gadget": nil, "snaps": mockCore20ModelSnaps}, "cannot remodel to Ubuntu Core 20 models yet"}, 152 } { 153 mergeMockModelHeaders(cur, t.new) 154 new := s.brands.Model(t.new["brand"].(string), t.new["model"].(string), t.new) 155 chg, err := devicestate.Remodel(s.state, new) 156 c.Check(chg, IsNil) 157 c.Check(err, ErrorMatches, t.errStr) 158 } 159 160 restore = devicestate.AllowUC20RemodelTesting(true) 161 defer restore() 162 163 for _, t := range []struct { 164 new map[string]interface{} 165 errStr string 166 }{ 167 // pre-UC20 to UC20 168 {map[string]interface{}{"base": "core20", "kernel": nil, "gadget": nil, "snaps": mockCore20ModelSnaps}, "cannot remodel from grade unset to grade signed"}, 169 } { 170 mergeMockModelHeaders(cur, t.new) 171 new := s.brands.Model(t.new["brand"].(string), t.new["model"].(string), t.new) 172 chg, err := devicestate.Remodel(s.state, new) 173 c.Check(chg, IsNil) 174 c.Check(err, ErrorMatches, t.errStr) 175 } 176 177 } 178 179 func (s *deviceMgrRemodelSuite) TestRemodelCheckGrade(c *C) { 180 s.state.Lock() 181 defer s.state.Unlock() 182 s.state.Set("seeded", true) 183 184 restore := devicestate.AllowUC20RemodelTesting(true) 185 defer restore() 186 187 // set a model assertion 188 cur := mockCore20ModelHeaders 189 s.makeModelAssertionInState(c, cur["brand"].(string), cur["model"].(string), map[string]interface{}{ 190 "architecture": cur["architecture"], 191 "base": cur["base"], 192 "grade": cur["grade"], 193 "snaps": cur["snaps"], 194 }) 195 s.makeSerialAssertionInState(c, cur["brand"].(string), cur["model"].(string), "orig-serial") 196 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 197 Brand: cur["brand"].(string), 198 Model: cur["model"].(string), 199 Serial: "orig-serial", 200 }) 201 202 // ensure all error cases are checked 203 for idx, t := range []struct { 204 new map[string]interface{} 205 errStr string 206 }{ 207 // uc20 model 208 {map[string]interface{}{"grade": "signed"}, "cannot remodel from grade dangerous to grade signed"}, 209 {map[string]interface{}{"grade": "secured"}, "cannot remodel from grade dangerous to grade secured"}, 210 // non-uc20 model 211 {map[string]interface{}{"snaps": nil, "grade": nil, "base": "core", "gadget": "pc", "kernel": "pc-kernel"}, "cannot remodel from grade dangerous to grade unset"}, 212 } { 213 c.Logf("tc: %v", idx) 214 mergeMockModelHeaders(cur, t.new) 215 new := s.brands.Model(t.new["brand"].(string), t.new["model"].(string), t.new) 216 chg, err := devicestate.Remodel(s.state, new) 217 c.Check(chg, IsNil) 218 c.Check(err, ErrorMatches, t.errStr) 219 } 220 } 221 222 func (s *deviceMgrRemodelSuite) TestRemodelRequiresSerial(c *C) { 223 s.state.Lock() 224 defer s.state.Unlock() 225 s.state.Set("seeded", true) 226 227 // set a model assertion 228 cur := map[string]interface{}{ 229 "brand": "canonical", 230 "model": "pc-model", 231 "architecture": "amd64", 232 "kernel": "pc-kernel", 233 "gadget": "pc", 234 } 235 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 236 "architecture": "amd64", 237 "kernel": "pc-kernel", 238 "gadget": "pc", 239 }) 240 // no serial assertion, no serial in state 241 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 242 Brand: "canonical", 243 Model: "pc-model", 244 }) 245 246 newModelHdrs := map[string]interface{}{ 247 "revision": "2", 248 } 249 mergeMockModelHeaders(cur, newModelHdrs) 250 new := s.brands.Model("canonical", "pc-model", newModelHdrs) 251 chg, err := devicestate.Remodel(s.state, new) 252 c.Check(chg, IsNil) 253 c.Check(err, ErrorMatches, "cannot remodel without a serial") 254 } 255 256 func (s *deviceMgrRemodelSuite) TestRemodelTasksSwitchGadgetTrack(c *C) { 257 s.testRemodelTasksSwitchTrack(c, "pc", map[string]interface{}{ 258 "gadget": "pc=18", 259 }) 260 } 261 262 func (s *deviceMgrRemodelSuite) TestRemodelTasksSwitchKernelTrack(c *C) { 263 s.testRemodelTasksSwitchTrack(c, "pc-kernel", map[string]interface{}{ 264 "kernel": "pc-kernel=18", 265 }) 266 } 267 268 func (s *deviceMgrRemodelSuite) testRemodelTasksSwitchTrack(c *C, whatRefreshes string, newModelOverrides map[string]interface{}) { 269 s.state.Lock() 270 defer s.state.Unlock() 271 s.state.Set("seeded", true) 272 s.state.Set("refresh-privacy-key", "some-privacy-key") 273 274 var testDeviceCtx snapstate.DeviceContext 275 276 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) { 277 c.Check(flags.Required, Equals, true) 278 c.Check(deviceCtx, Equals, testDeviceCtx) 279 c.Check(fromChange, Equals, "99") 280 281 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 282 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 283 tValidate.WaitFor(tDownload) 284 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 285 tInstall.WaitFor(tValidate) 286 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 287 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 288 return ts, nil 289 }) 290 defer restore() 291 292 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) { 293 c.Check(flags.Required, Equals, false) 294 c.Check(flags.NoReRefresh, Equals, true) 295 c.Check(deviceCtx, Equals, testDeviceCtx) 296 c.Check(fromChange, Equals, "99") 297 c.Check(name, Equals, whatRefreshes) 298 c.Check(opts.Channel, Equals, "18") 299 300 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s to track %s", name, opts.Channel)) 301 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 302 tValidate.WaitFor(tDownload) 303 tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) 304 tUpdate.WaitFor(tValidate) 305 ts := state.NewTaskSet(tDownload, tValidate, tUpdate) 306 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 307 return ts, nil 308 }) 309 defer restore() 310 311 // set a model assertion 312 current := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 313 "architecture": "amd64", 314 "kernel": "pc-kernel", 315 "gadget": "pc", 316 "base": "core18", 317 }) 318 err := assertstate.Add(s.state, current) 319 c.Assert(err, IsNil) 320 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 321 Brand: "canonical", 322 Model: "pc-model", 323 }) 324 325 headers := map[string]interface{}{ 326 "architecture": "amd64", 327 "kernel": "pc-kernel", 328 "gadget": "pc", 329 "base": "core18", 330 "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"}, 331 "revision": "1", 332 } 333 for k, v := range newModelOverrides { 334 headers[k] = v 335 } 336 new := s.brands.Model("canonical", "pc-model", headers) 337 338 testDeviceCtx = &snapstatetest.TrivialDeviceContext{Remodeling: true} 339 340 tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99") 341 c.Assert(err, IsNil) 342 // 2 snaps, plus one track switch plus the remodel task, the 343 // wait chain is tested in TestRemodel* 344 c.Assert(tss, HasLen, 4) 345 } 346 347 func (s *deviceMgrRemodelSuite) TestRemodelTasksSwitchGadget(c *C) { 348 s.testRemodelSwitchTasks(c, "other-gadget", "18", map[string]interface{}{ 349 "gadget": "other-gadget=18", 350 }) 351 } 352 353 func (s *deviceMgrRemodelSuite) TestRemodelTasksSwitchKernel(c *C) { 354 s.testRemodelSwitchTasks(c, "other-kernel", "18", map[string]interface{}{ 355 "kernel": "other-kernel=18", 356 }) 357 } 358 359 func (s *deviceMgrRemodelSuite) testRemodelSwitchTasks(c *C, whatsNew, whatNewTrack string, newModelOverrides map[string]interface{}) { 360 c.Check(newModelOverrides, HasLen, 1, Commentf("test expects a single model property to change")) 361 s.state.Lock() 362 defer s.state.Unlock() 363 s.state.Set("seeded", true) 364 s.state.Set("refresh-privacy-key", "some-privacy-key") 365 366 var testDeviceCtx snapstate.DeviceContext 367 368 var snapstateInstallWithDeviceContextCalled int 369 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) { 370 snapstateInstallWithDeviceContextCalled++ 371 c.Check(name, Equals, whatsNew) 372 if whatNewTrack != "" { 373 c.Check(opts.Channel, Equals, whatNewTrack) 374 } 375 376 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 377 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 378 tValidate.WaitFor(tDownload) 379 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 380 tInstall.WaitFor(tValidate) 381 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 382 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 383 return ts, nil 384 }) 385 defer restore() 386 387 // set a model assertion 388 current := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 389 "architecture": "amd64", 390 "kernel": "pc-kernel", 391 "gadget": "pc", 392 "base": "core18", 393 }) 394 err := assertstate.Add(s.state, current) 395 c.Assert(err, IsNil) 396 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 397 Brand: "canonical", 398 Model: "pc-model", 399 }) 400 401 headers := map[string]interface{}{ 402 "architecture": "amd64", 403 "kernel": "pc-kernel", 404 "gadget": "pc", 405 "base": "core18", 406 "revision": "1", 407 } 408 for k, v := range newModelOverrides { 409 headers[k] = v 410 } 411 new := s.brands.Model("canonical", "pc-model", headers) 412 413 testDeviceCtx = &snapstatetest.TrivialDeviceContext{Remodeling: true} 414 415 tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99") 416 c.Assert(err, IsNil) 417 // 1 of switch-kernel/base/gadget plus the remodel task 418 c.Assert(tss, HasLen, 2) 419 // API was hit 420 c.Assert(snapstateInstallWithDeviceContextCalled, Equals, 1) 421 } 422 423 func (s *deviceMgrRemodelSuite) TestRemodelRequiredSnaps(c *C) { 424 s.state.Lock() 425 defer s.state.Unlock() 426 s.state.Set("seeded", true) 427 s.state.Set("refresh-privacy-key", "some-privacy-key") 428 429 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) { 430 c.Check(flags.Required, Equals, true) 431 c.Check(deviceCtx, NotNil) 432 c.Check(deviceCtx.ForRemodeling(), Equals, true) 433 434 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 435 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 436 tValidate.WaitFor(tDownload) 437 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 438 tInstall.WaitFor(tValidate) 439 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 440 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 441 return ts, nil 442 }) 443 defer restore() 444 445 // set a model assertion 446 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 447 "architecture": "amd64", 448 "kernel": "pc-kernel", 449 "gadget": "pc", 450 "base": "core18", 451 }) 452 s.makeSerialAssertionInState(c, "canonical", "pc-model", "1234") 453 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 454 Brand: "canonical", 455 Model: "pc-model", 456 Serial: "1234", 457 }) 458 459 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 460 "architecture": "amd64", 461 "kernel": "pc-kernel", 462 "gadget": "pc", 463 "base": "core18", 464 "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"}, 465 "revision": "1", 466 }) 467 chg, err := devicestate.Remodel(s.state, new) 468 c.Assert(err, IsNil) 469 c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") 470 471 tl := chg.Tasks() 472 // 2 snaps, 473 c.Assert(tl, HasLen, 2*3+1) 474 475 deviceCtx, err := devicestate.DeviceCtx(s.state, tl[0], nil) 476 c.Assert(err, IsNil) 477 // deviceCtx is actually a remodelContext here 478 remodCtx, ok := deviceCtx.(devicestate.RemodelContext) 479 c.Assert(ok, Equals, true) 480 c.Check(remodCtx.ForRemodeling(), Equals, true) 481 c.Check(remodCtx.Kind(), Equals, devicestate.UpdateRemodel) 482 c.Check(remodCtx.Model(), DeepEquals, new) 483 c.Check(remodCtx.Store(), IsNil) 484 485 // check the tasks 486 tDownloadSnap1 := tl[0] 487 tValidateSnap1 := tl[1] 488 tInstallSnap1 := tl[2] 489 tDownloadSnap2 := tl[3] 490 tValidateSnap2 := tl[4] 491 tInstallSnap2 := tl[5] 492 tSetModel := tl[6] 493 494 // check the tasks 495 c.Assert(tDownloadSnap1.Kind(), Equals, "fake-download") 496 c.Assert(tDownloadSnap1.Summary(), Equals, "Download new-required-snap-1") 497 c.Assert(tDownloadSnap1.WaitTasks(), HasLen, 0) 498 c.Assert(tValidateSnap1.Kind(), Equals, "validate-snap") 499 c.Assert(tValidateSnap1.Summary(), Equals, "Validate new-required-snap-1") 500 c.Assert(tDownloadSnap1.WaitTasks(), HasLen, 0) 501 c.Assert(tDownloadSnap2.Kind(), Equals, "fake-download") 502 c.Assert(tDownloadSnap2.Summary(), Equals, "Download new-required-snap-2") 503 // check the ordering, download/validate everything first, then install 504 505 // snap2 downloads wait for the downloads of snap1 506 c.Assert(tDownloadSnap1.WaitTasks(), HasLen, 0) 507 c.Assert(tValidateSnap1.WaitTasks(), DeepEquals, []*state.Task{ 508 tDownloadSnap1, 509 }) 510 c.Assert(tDownloadSnap2.WaitTasks(), DeepEquals, []*state.Task{ 511 tValidateSnap1, 512 }) 513 c.Assert(tValidateSnap2.WaitTasks(), DeepEquals, []*state.Task{ 514 tDownloadSnap2, 515 }) 516 c.Assert(tInstallSnap1.WaitTasks(), DeepEquals, []*state.Task{ 517 // wait for own check-snap 518 tValidateSnap1, 519 // and also the last check-snap of the download chain 520 tValidateSnap2, 521 }) 522 c.Assert(tInstallSnap2.WaitTasks(), DeepEquals, []*state.Task{ 523 // last snap of the download chain 524 tValidateSnap2, 525 // previous install chain 526 tInstallSnap1, 527 }) 528 529 c.Assert(tSetModel.Kind(), Equals, "set-model") 530 c.Assert(tSetModel.Summary(), Equals, "Set new model assertion") 531 // setModel waits for everything in the change 532 c.Assert(tSetModel.WaitTasks(), DeepEquals, []*state.Task{tDownloadSnap1, tValidateSnap1, tInstallSnap1, tDownloadSnap2, tValidateSnap2, tInstallSnap2}) 533 } 534 535 func (s *deviceMgrRemodelSuite) TestRemodelSwitchKernelTrack(c *C) { 536 s.state.Lock() 537 defer s.state.Unlock() 538 s.state.Set("seeded", true) 539 s.state.Set("refresh-privacy-key", "some-privacy-key") 540 541 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) { 542 c.Check(flags.Required, Equals, true) 543 c.Check(deviceCtx, NotNil) 544 c.Check(deviceCtx.ForRemodeling(), Equals, true) 545 546 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 547 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 548 tValidate.WaitFor(tDownload) 549 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 550 tInstall.WaitFor(tValidate) 551 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 552 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 553 return ts, nil 554 }) 555 defer restore() 556 557 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) { 558 c.Check(flags.Required, Equals, false) 559 c.Check(flags.NoReRefresh, Equals, true) 560 c.Check(deviceCtx, NotNil) 561 c.Check(deviceCtx.ForRemodeling(), Equals, true) 562 563 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s to track %s", name, opts.Channel)) 564 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 565 tValidate.WaitFor(tDownload) 566 tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) 567 tUpdate.WaitFor(tValidate) 568 ts := state.NewTaskSet(tDownload, tValidate, tUpdate) 569 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 570 return ts, nil 571 }) 572 defer restore() 573 574 // set a model assertion 575 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 576 "architecture": "amd64", 577 "kernel": "pc-kernel", 578 "gadget": "pc", 579 "base": "core18", 580 }) 581 s.makeSerialAssertionInState(c, "canonical", "pc-model", "1234") 582 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 583 Brand: "canonical", 584 Model: "pc-model", 585 Serial: "1234", 586 }) 587 588 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 589 "architecture": "amd64", 590 "kernel": "pc-kernel=18", 591 "gadget": "pc", 592 "base": "core18", 593 "required-snaps": []interface{}{"new-required-snap-1"}, 594 "revision": "1", 595 }) 596 chg, err := devicestate.Remodel(s.state, new) 597 c.Assert(err, IsNil) 598 c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") 599 600 tl := chg.Tasks() 601 c.Assert(tl, HasLen, 2*3+1) 602 603 tDownloadKernel := tl[0] 604 tValidateKernel := tl[1] 605 tUpdateKernel := tl[2] 606 tDownloadSnap1 := tl[3] 607 tValidateSnap1 := tl[4] 608 tInstallSnap1 := tl[5] 609 tSetModel := tl[6] 610 611 c.Assert(tDownloadKernel.Kind(), Equals, "fake-download") 612 c.Assert(tDownloadKernel.Summary(), Equals, "Download pc-kernel to track 18") 613 c.Assert(tValidateKernel.Kind(), Equals, "validate-snap") 614 c.Assert(tValidateKernel.Summary(), Equals, "Validate pc-kernel") 615 c.Assert(tUpdateKernel.Kind(), Equals, "fake-update") 616 c.Assert(tUpdateKernel.Summary(), Equals, "Update pc-kernel to track 18") 617 c.Assert(tDownloadSnap1.Kind(), Equals, "fake-download") 618 c.Assert(tDownloadSnap1.Summary(), Equals, "Download new-required-snap-1") 619 c.Assert(tValidateSnap1.Kind(), Equals, "validate-snap") 620 c.Assert(tValidateSnap1.Summary(), Equals, "Validate new-required-snap-1") 621 c.Assert(tInstallSnap1.Kind(), Equals, "fake-install") 622 c.Assert(tInstallSnap1.Summary(), Equals, "Install new-required-snap-1") 623 624 c.Assert(tSetModel.Kind(), Equals, "set-model") 625 c.Assert(tSetModel.Summary(), Equals, "Set new model assertion") 626 627 // check the ordering 628 c.Assert(tDownloadSnap1.WaitTasks(), DeepEquals, []*state.Task{ 629 // previous download finished 630 tValidateKernel, 631 }) 632 c.Assert(tInstallSnap1.WaitTasks(), DeepEquals, []*state.Task{ 633 // last download in the chain finished 634 tValidateSnap1, 635 // and kernel got updated 636 tUpdateKernel, 637 }) 638 c.Assert(tUpdateKernel.WaitTasks(), DeepEquals, []*state.Task{ 639 // kernel is valid 640 tValidateKernel, 641 // and last download in the chain finished 642 tValidateSnap1, 643 }) 644 } 645 646 func (s *deviceMgrRemodelSuite) TestRemodelLessRequiredSnaps(c *C) { 647 s.state.Lock() 648 defer s.state.Unlock() 649 s.state.Set("seeded", true) 650 s.state.Set("refresh-privacy-key", "some-privacy-key") 651 652 // set a model assertion 653 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 654 "architecture": "amd64", 655 "kernel": "pc-kernel", 656 "gadget": "pc", 657 "base": "core18", 658 "required-snaps": []interface{}{"some-required-snap"}, 659 }) 660 s.makeSerialAssertionInState(c, "canonical", "pc-model", "1234") 661 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 662 Brand: "canonical", 663 Model: "pc-model", 664 Serial: "1234", 665 }) 666 667 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 668 "architecture": "amd64", 669 "kernel": "pc-kernel", 670 "gadget": "pc", 671 "base": "core18", 672 "revision": "1", 673 }) 674 chg, err := devicestate.Remodel(s.state, new) 675 c.Assert(err, IsNil) 676 c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") 677 678 tl := chg.Tasks() 679 c.Assert(tl, HasLen, 1) 680 tSetModel := tl[0] 681 c.Assert(tSetModel.Kind(), Equals, "set-model") 682 c.Assert(tSetModel.Summary(), Equals, "Set new model assertion") 683 } 684 685 type freshSessionStore struct { 686 storetest.Store 687 688 ensureDeviceSession int 689 } 690 691 func (sto *freshSessionStore) EnsureDeviceSession() (*auth.DeviceState, error) { 692 sto.ensureDeviceSession += 1 693 return nil, nil 694 } 695 696 func (s *deviceMgrRemodelSuite) TestRemodelStoreSwitch(c *C) { 697 s.state.Lock() 698 defer s.state.Unlock() 699 s.state.Set("seeded", true) 700 s.state.Set("refresh-privacy-key", "some-privacy-key") 701 702 var testStore snapstate.StoreService 703 704 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) { 705 c.Check(flags.Required, Equals, true) 706 c.Check(deviceCtx, NotNil) 707 c.Check(deviceCtx.ForRemodeling(), Equals, true) 708 709 c.Check(deviceCtx.Store(), Equals, testStore) 710 711 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 712 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 713 tValidate.WaitFor(tDownload) 714 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 715 tInstall.WaitFor(tValidate) 716 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 717 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 718 return ts, nil 719 }) 720 defer restore() 721 722 // set a model assertion 723 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 724 "architecture": "amd64", 725 "kernel": "pc-kernel", 726 "gadget": "pc", 727 "base": "core18", 728 }) 729 s.makeSerialAssertionInState(c, "canonical", "pc-model", "1234") 730 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 731 Brand: "canonical", 732 Model: "pc-model", 733 Serial: "1234", 734 }) 735 736 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 737 "architecture": "amd64", 738 "kernel": "pc-kernel", 739 "gadget": "pc", 740 "base": "core18", 741 "store": "switched-store", 742 "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"}, 743 "revision": "1", 744 }) 745 746 freshStore := &freshSessionStore{} 747 testStore = freshStore 748 749 s.newFakeStore = func(devBE storecontext.DeviceBackend) snapstate.StoreService { 750 mod, err := devBE.Model() 751 c.Check(err, IsNil) 752 if err == nil { 753 c.Check(mod, DeepEquals, new) 754 } 755 return testStore 756 } 757 758 chg, err := devicestate.Remodel(s.state, new) 759 c.Assert(err, IsNil) 760 c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") 761 762 c.Check(freshStore.ensureDeviceSession, Equals, 1) 763 764 tl := chg.Tasks() 765 // 2 snaps * 3 tasks (from the mock install above) + 766 // 1 "set-model" task at the end 767 c.Assert(tl, HasLen, 2*3+1) 768 769 deviceCtx, err := devicestate.DeviceCtx(s.state, tl[0], nil) 770 c.Assert(err, IsNil) 771 // deviceCtx is actually a remodelContext here 772 remodCtx, ok := deviceCtx.(devicestate.RemodelContext) 773 c.Assert(ok, Equals, true) 774 c.Check(remodCtx.ForRemodeling(), Equals, true) 775 c.Check(remodCtx.Kind(), Equals, devicestate.StoreSwitchRemodel) 776 c.Check(remodCtx.Model(), DeepEquals, new) 777 c.Check(remodCtx.Store(), Equals, testStore) 778 } 779 780 func (s *deviceMgrRemodelSuite) TestRemodelRereg(c *C) { 781 s.state.Lock() 782 defer s.state.Unlock() 783 s.state.Set("seeded", true) 784 785 // set a model assertion 786 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 787 "architecture": "amd64", 788 "kernel": "pc-kernel", 789 "gadget": "pc", 790 "base": "core18", 791 }) 792 s.makeSerialAssertionInState(c, "canonical", "pc-model", "orig-serial") 793 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 794 Brand: "canonical", 795 Model: "pc-model", 796 Serial: "orig-serial", 797 SessionMacaroon: "old-session", 798 }) 799 800 new := s.brands.Model("canonical", "rereg-model", map[string]interface{}{ 801 "architecture": "amd64", 802 "kernel": "pc-kernel", 803 "gadget": "pc", 804 "base": "core18", 805 "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"}, 806 }) 807 808 s.newFakeStore = func(devBE storecontext.DeviceBackend) snapstate.StoreService { 809 mod, err := devBE.Model() 810 c.Check(err, IsNil) 811 if err == nil { 812 c.Check(mod, DeepEquals, new) 813 } 814 return nil 815 } 816 817 chg, err := devicestate.Remodel(s.state, new) 818 c.Assert(err, IsNil) 819 820 c.Assert(chg.Summary(), Equals, "Remodel device to canonical/rereg-model (0)") 821 822 tl := chg.Tasks() 823 c.Assert(tl, HasLen, 2) 824 825 // check the tasks 826 tRequestSerial := tl[0] 827 tPrepareRemodeling := tl[1] 828 829 // check the tasks 830 c.Assert(tRequestSerial.Kind(), Equals, "request-serial") 831 c.Assert(tRequestSerial.Summary(), Equals, "Request new device serial") 832 c.Assert(tRequestSerial.WaitTasks(), HasLen, 0) 833 834 c.Assert(tPrepareRemodeling.Kind(), Equals, "prepare-remodeling") 835 c.Assert(tPrepareRemodeling.Summary(), Equals, "Prepare remodeling") 836 c.Assert(tPrepareRemodeling.WaitTasks(), DeepEquals, []*state.Task{tRequestSerial}) 837 } 838 839 func (s *deviceMgrRemodelSuite) TestRemodelClash(c *C) { 840 s.state.Lock() 841 defer s.state.Unlock() 842 s.state.Set("seeded", true) 843 s.state.Set("refresh-privacy-key", "some-privacy-key") 844 845 var clashing *asserts.Model 846 847 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) { 848 // simulate things changing under our feet 849 assertstatetest.AddMany(st, clashing) 850 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 851 Brand: "canonical", 852 Model: clashing.Model(), 853 }) 854 855 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 856 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 857 tValidate.WaitFor(tDownload) 858 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 859 tInstall.WaitFor(tValidate) 860 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 861 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 862 return ts, nil 863 }) 864 defer restore() 865 866 // set a model assertion 867 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 868 "architecture": "amd64", 869 "kernel": "pc-kernel", 870 "gadget": "pc", 871 "base": "core18", 872 }) 873 s.makeSerialAssertionInState(c, "canonical", "pc-model", "1234") 874 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 875 Brand: "canonical", 876 Model: "pc-model", 877 Serial: "1234", 878 }) 879 880 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 881 "architecture": "amd64", 882 "kernel": "pc-kernel", 883 "gadget": "pc", 884 "base": "core18", 885 "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"}, 886 "revision": "1", 887 }) 888 other := s.brands.Model("canonical", "pc-model-other", map[string]interface{}{ 889 "architecture": "amd64", 890 "kernel": "pc-kernel", 891 "gadget": "pc", 892 "base": "core18", 893 "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"}, 894 }) 895 896 clashing = other 897 _, err := devicestate.Remodel(s.state, new) 898 c.Check(err, DeepEquals, &snapstate.ChangeConflictError{ 899 Message: "cannot start remodel, clashing with concurrent remodel to canonical/pc-model-other (0)", 900 }) 901 902 // reset 903 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 904 Brand: "canonical", 905 Model: "pc-model", 906 Serial: "1234", 907 }) 908 clashing = new 909 _, err = devicestate.Remodel(s.state, new) 910 c.Check(err, DeepEquals, &snapstate.ChangeConflictError{ 911 Message: "cannot start remodel, clashing with concurrent remodel to canonical/pc-model (1)", 912 }) 913 } 914 915 func (s *deviceMgrRemodelSuite) TestRemodelClashInProgress(c *C) { 916 s.state.Lock() 917 defer s.state.Unlock() 918 s.state.Set("seeded", true) 919 s.state.Set("refresh-privacy-key", "some-privacy-key") 920 921 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) { 922 // simulate another started remodeling 923 st.NewChange("remodel", "other remodel") 924 925 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 926 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 927 tValidate.WaitFor(tDownload) 928 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 929 tInstall.WaitFor(tValidate) 930 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 931 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 932 return ts, nil 933 }) 934 defer restore() 935 936 // set a model assertion 937 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 938 "architecture": "amd64", 939 "kernel": "pc-kernel", 940 "gadget": "pc", 941 "base": "core18", 942 }) 943 s.makeSerialAssertionInState(c, "canonical", "pc-model", "1234") 944 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 945 Brand: "canonical", 946 Model: "pc-model", 947 Serial: "1234", 948 }) 949 950 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 951 "architecture": "amd64", 952 "kernel": "pc-kernel", 953 "gadget": "pc", 954 "base": "core18", 955 "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"}, 956 "revision": "1", 957 }) 958 959 _, err := devicestate.Remodel(s.state, new) 960 c.Check(err, DeepEquals, &snapstate.ChangeConflictError{ 961 Message: "cannot start remodel, clashing with concurrent one", 962 }) 963 } 964 965 func (s *deviceMgrRemodelSuite) TestReregRemodelClashAnyChange(c *C) { 966 s.state.Lock() 967 defer s.state.Unlock() 968 s.state.Set("seeded", true) 969 970 // set a model assertion 971 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 972 "architecture": "amd64", 973 "kernel": "pc-kernel", 974 "gadget": "pc", 975 "base": "core18", 976 }) 977 s.makeSerialAssertionInState(c, "canonical", "pc-model", "orig-serial") 978 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 979 Brand: "canonical", 980 Model: "pc-model", 981 Serial: "orig-serial", 982 SessionMacaroon: "old-session", 983 }) 984 985 new := s.brands.Model("canonical", "pc-model-2", map[string]interface{}{ 986 "architecture": "amd64", 987 "kernel": "pc-kernel", 988 "gadget": "pc", 989 "base": "core18", 990 "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"}, 991 "revision": "1", 992 }) 993 s.newFakeStore = func(devBE storecontext.DeviceBackend) snapstate.StoreService { 994 // we never reach the place where this gets called 995 c.Fatalf("unexpected call") 996 return nil 997 } 998 999 // simulate any other change 1000 chg := s.state.NewChange("chg", "other change") 1001 chg.SetStatus(state.DoingStatus) 1002 1003 _, err := devicestate.Remodel(s.state, new) 1004 c.Assert(err, NotNil) 1005 c.Assert(err, DeepEquals, &snapstate.ChangeConflictError{ 1006 ChangeKind: "chg", 1007 Message: `other changes in progress (conflicting change "chg"), change "remodel" not allowed until they are done`, 1008 }) 1009 } 1010 1011 func (s *deviceMgrRemodelSuite) TestRemodeling(c *C) { 1012 s.state.Lock() 1013 defer s.state.Unlock() 1014 1015 // no changes 1016 c.Check(devicestate.Remodeling(s.state), Equals, false) 1017 1018 // other change 1019 s.state.NewChange("other", "...") 1020 c.Check(devicestate.Remodeling(s.state), Equals, false) 1021 1022 // remodel change 1023 chg := s.state.NewChange("remodel", "...") 1024 c.Check(devicestate.Remodeling(s.state), Equals, true) 1025 1026 // done 1027 chg.SetStatus(state.DoneStatus) 1028 c.Check(devicestate.Remodeling(s.state), Equals, false) 1029 } 1030 1031 func (s *deviceMgrRemodelSuite) TestDeviceCtxNoTask(c *C) { 1032 s.state.Lock() 1033 defer s.state.Unlock() 1034 // nothing in the state 1035 1036 _, err := devicestate.DeviceCtx(s.state, nil, nil) 1037 c.Check(err, Equals, state.ErrNoState) 1038 1039 // have a model assertion 1040 model := s.brands.Model("canonical", "pc", map[string]interface{}{ 1041 "gadget": "pc", 1042 "kernel": "kernel", 1043 "architecture": "amd64", 1044 }) 1045 assertstatetest.AddMany(s.state, model) 1046 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 1047 Brand: "canonical", 1048 Model: "pc", 1049 }) 1050 1051 deviceCtx, err := devicestate.DeviceCtx(s.state, nil, nil) 1052 c.Assert(err, IsNil) 1053 c.Assert(deviceCtx.Model().BrandID(), Equals, "canonical") 1054 1055 c.Check(deviceCtx.Classic(), Equals, false) 1056 c.Check(deviceCtx.Kernel(), Equals, "kernel") 1057 c.Check(deviceCtx.Base(), Equals, "") 1058 c.Check(deviceCtx.RunMode(), Equals, true) 1059 // not a uc20 model, so no modeenv 1060 c.Check(deviceCtx.HasModeenv(), Equals, false) 1061 } 1062 1063 func (s *deviceMgrRemodelSuite) TestDeviceCtxGroundContext(c *C) { 1064 s.state.Lock() 1065 defer s.state.Unlock() 1066 1067 // have a model assertion 1068 model := s.brands.Model("canonical", "pc", map[string]interface{}{ 1069 "gadget": "pc", 1070 "kernel": "kernel", 1071 "architecture": "amd64", 1072 }) 1073 assertstatetest.AddMany(s.state, model) 1074 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 1075 Brand: "canonical", 1076 Model: "pc", 1077 }) 1078 1079 deviceCtx, err := devicestate.DeviceCtx(s.state, nil, nil) 1080 c.Assert(err, IsNil) 1081 c.Assert(deviceCtx.Model().BrandID(), Equals, "canonical") 1082 groundCtx := deviceCtx.GroundContext() 1083 c.Check(groundCtx.ForRemodeling(), Equals, false) 1084 c.Check(groundCtx.Model().Model(), Equals, "pc") 1085 c.Check(groundCtx.Store, PanicMatches, `retrieved ground context is not intended to drive store operations`) 1086 } 1087 1088 func (s *deviceMgrRemodelSuite) TestDeviceCtxProvided(c *C) { 1089 s.state.Lock() 1090 defer s.state.Unlock() 1091 1092 model := assertstest.FakeAssertion(map[string]interface{}{ 1093 "type": "model", 1094 "authority-id": "canonical", 1095 "series": "16", 1096 "brand-id": "canonical", 1097 "model": "pc", 1098 "gadget": "pc", 1099 "kernel": "kernel", 1100 "architecture": "amd64", 1101 }).(*asserts.Model) 1102 1103 deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: model} 1104 1105 deviceCtx1, err := devicestate.DeviceCtx(s.state, nil, deviceCtx) 1106 c.Assert(err, IsNil) 1107 c.Assert(deviceCtx1, Equals, deviceCtx) 1108 } 1109 1110 func (s *deviceMgrRemodelSuite) TestCheckGadgetRemodelCompatible(c *C) { 1111 s.state.Lock() 1112 defer s.state.Unlock() 1113 1114 currentSnapYaml := ` 1115 name: gadget 1116 type: gadget 1117 version: 123 1118 ` 1119 remodelSnapYaml := ` 1120 name: new-gadget 1121 type: gadget 1122 version: 123 1123 ` 1124 mockGadget := ` 1125 type: gadget 1126 name: gadget 1127 volumes: 1128 volume: 1129 schema: gpt 1130 bootloader: grub 1131 ` 1132 siCurrent := &snap.SideInfo{Revision: snap.R(123), RealName: "gadget"} 1133 // so that we get a directory 1134 currInfo := snaptest.MockSnapWithFiles(c, currentSnapYaml, siCurrent, nil) 1135 info := snaptest.MockSnapWithFiles(c, remodelSnapYaml, &snap.SideInfo{Revision: snap.R(1)}, nil) 1136 snapf, err := snapfile.Open(info.MountDir()) 1137 c.Assert(err, IsNil) 1138 1139 s.setupBrands(c) 1140 1141 oldModel := fakeMyModel(map[string]interface{}{ 1142 "architecture": "amd64", 1143 "gadget": "gadget", 1144 "kernel": "kernel", 1145 }) 1146 deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: oldModel} 1147 1148 // model assertion in device context 1149 newModel := fakeMyModel(map[string]interface{}{ 1150 "architecture": "amd64", 1151 "gadget": "new-gadget", 1152 "kernel": "kernel", 1153 }) 1154 remodelCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: newModel, Remodeling: true, OldDeviceModel: oldModel} 1155 1156 restore := devicestate.MockGadgetIsCompatible(func(current, update *gadget.Info) error { 1157 c.Assert(current.Volumes, HasLen, 1) 1158 c.Assert(update.Volumes, HasLen, 1) 1159 return errors.New("fail") 1160 }) 1161 defer restore() 1162 1163 // not on classic 1164 release.OnClassic = true 1165 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, currInfo, snapf, snapstate.Flags{}, remodelCtx) 1166 c.Check(err, IsNil) 1167 release.OnClassic = false 1168 1169 // nothing if not remodeling 1170 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, currInfo, snapf, snapstate.Flags{}, deviceCtx) 1171 c.Check(err, IsNil) 1172 1173 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, currInfo, snapf, snapstate.Flags{}, remodelCtx) 1174 c.Check(err, ErrorMatches, "cannot read new gadget metadata: .*/new-gadget/1/meta/gadget.yaml: no such file or directory") 1175 1176 // drop gadget.yaml to the new gadget 1177 err = ioutil.WriteFile(filepath.Join(info.MountDir(), "meta/gadget.yaml"), []byte(mockGadget), 0644) 1178 c.Assert(err, IsNil) 1179 1180 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, currInfo, snapf, snapstate.Flags{}, remodelCtx) 1181 c.Check(err, ErrorMatches, "cannot read current gadget metadata: .*/gadget/123/meta/gadget.yaml: no such file or directory") 1182 1183 // drop gadget.yaml to the current gadget 1184 err = ioutil.WriteFile(filepath.Join(currInfo.MountDir(), "meta/gadget.yaml"), []byte(mockGadget), 0644) 1185 c.Assert(err, IsNil) 1186 1187 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, currInfo, snapf, snapstate.Flags{}, remodelCtx) 1188 c.Check(err, ErrorMatches, "cannot remodel to an incompatible gadget: fail") 1189 1190 restore = devicestate.MockGadgetIsCompatible(func(current, update *gadget.Info) error { 1191 c.Assert(current.Volumes, HasLen, 1) 1192 c.Assert(update.Volumes, HasLen, 1) 1193 return nil 1194 }) 1195 defer restore() 1196 1197 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, currInfo, snapf, snapstate.Flags{}, remodelCtx) 1198 c.Check(err, IsNil) 1199 1200 // when remodeling to completely new gadget snap, there is no current 1201 // snap passed to the check callback 1202 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, nil, snapf, snapstate.Flags{}, remodelCtx) 1203 c.Check(err, ErrorMatches, "cannot identify the current gadget snap") 1204 1205 // mock data to obtain current gadget info 1206 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 1207 Brand: "canonical", 1208 Model: "gadget", 1209 }) 1210 s.makeModelAssertionInState(c, "canonical", "gadget", map[string]interface{}{ 1211 "architecture": "amd64", 1212 "kernel": "kernel", 1213 "gadget": "gadget", 1214 }) 1215 1216 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, nil, snapf, snapstate.Flags{}, remodelCtx) 1217 c.Check(err, ErrorMatches, "cannot identify the current gadget snap") 1218 1219 snapstate.Set(s.state, "gadget", &snapstate.SnapState{ 1220 SnapType: "gadget", 1221 Sequence: []*snap.SideInfo{siCurrent}, 1222 Current: siCurrent.Revision, 1223 Active: true, 1224 }) 1225 1226 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, nil, snapf, snapstate.Flags{}, remodelCtx) 1227 c.Check(err, IsNil) 1228 } 1229 1230 var ( 1231 compatibleTestMockOkGadget = ` 1232 type: gadget 1233 name: gadget 1234 volumes: 1235 volume: 1236 schema: gpt 1237 bootloader: grub 1238 structure: 1239 - name: foo 1240 size: 10M 1241 type: 00000000-0000-0000-0000-0000deadbeef 1242 ` 1243 ) 1244 1245 func (s *deviceMgrRemodelSuite) testCheckGadgetRemodelCompatibleWithYaml(c *C, currentGadgetYaml, newGadgetYaml string, expErr string) { 1246 s.state.Lock() 1247 defer s.state.Unlock() 1248 1249 currentSnapYaml := ` 1250 name: gadget 1251 type: gadget 1252 version: 123 1253 ` 1254 remodelSnapYaml := ` 1255 name: new-gadget 1256 type: gadget 1257 version: 123 1258 ` 1259 1260 currInfo := snaptest.MockSnapWithFiles(c, currentSnapYaml, &snap.SideInfo{Revision: snap.R(123)}, [][]string{ 1261 {"meta/gadget.yaml", currentGadgetYaml}, 1262 }) 1263 // gadget we're remodeling to is identical 1264 info := snaptest.MockSnapWithFiles(c, remodelSnapYaml, &snap.SideInfo{Revision: snap.R(1)}, [][]string{ 1265 {"meta/gadget.yaml", newGadgetYaml}, 1266 }) 1267 snapf, err := snapfile.Open(info.MountDir()) 1268 c.Assert(err, IsNil) 1269 1270 s.setupBrands(c) 1271 // model assertion in device context 1272 oldModel := fakeMyModel(map[string]interface{}{ 1273 "architecture": "amd64", 1274 "gadget": "new-gadget", 1275 "kernel": "krnl", 1276 }) 1277 model := fakeMyModel(map[string]interface{}{ 1278 "architecture": "amd64", 1279 "gadget": "new-gadget", 1280 "kernel": "krnl", 1281 }) 1282 remodelCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: model, Remodeling: true, OldDeviceModel: oldModel} 1283 1284 err = devicestate.CheckGadgetRemodelCompatible(s.state, info, currInfo, snapf, snapstate.Flags{}, remodelCtx) 1285 if expErr == "" { 1286 c.Check(err, IsNil) 1287 } else { 1288 c.Check(err, ErrorMatches, expErr) 1289 } 1290 1291 } 1292 1293 func (s *deviceMgrRemodelSuite) TestCheckGadgetRemodelCompatibleWithYamlHappy(c *C) { 1294 s.testCheckGadgetRemodelCompatibleWithYaml(c, compatibleTestMockOkGadget, compatibleTestMockOkGadget, "") 1295 } 1296 1297 func (s *deviceMgrRemodelSuite) TestCheckGadgetRemodelCompatibleWithYamlBad(c *C) { 1298 mockBadGadgetYaml := ` 1299 type: gadget 1300 name: gadget 1301 volumes: 1302 volume: 1303 schema: gpt 1304 bootloader: grub 1305 structure: 1306 - name: foo 1307 size: 20M 1308 type: 00000000-0000-0000-0000-0000deadbeef 1309 ` 1310 1311 errMatch := `cannot remodel to an incompatible gadget: incompatible layout change: incompatible structure #0 \("foo"\) change: cannot change structure size from 10485760 to 20971520` 1312 s.testCheckGadgetRemodelCompatibleWithYaml(c, compatibleTestMockOkGadget, mockBadGadgetYaml, errMatch) 1313 } 1314 1315 func (s *deviceMgrRemodelSuite) mockTasksNopHandler(kinds ...string) { 1316 nopHandler := func(task *state.Task, _ *tomb.Tomb) error { return nil } 1317 for _, kind := range kinds { 1318 s.o.TaskRunner().AddHandler(kind, nopHandler, nil) 1319 } 1320 } 1321 1322 func (s *deviceMgrRemodelSuite) TestRemodelGadgetAssetsUpdate(c *C) { 1323 var currentGadgetYaml = ` 1324 volumes: 1325 pc: 1326 bootloader: grub 1327 structure: 1328 - name: foo 1329 type: 00000000-0000-0000-0000-0000deadcafe 1330 filesystem: ext4 1331 size: 10M 1332 content: 1333 - source: foo-content 1334 target: / 1335 - name: bare-one 1336 type: bare 1337 size: 1M 1338 content: 1339 - image: bare.img 1340 ` 1341 1342 var remodelGadgetYaml = ` 1343 volumes: 1344 pc: 1345 bootloader: grub 1346 structure: 1347 - name: foo 1348 type: 00000000-0000-0000-0000-0000deadcafe 1349 filesystem: ext4 1350 size: 10M 1351 content: 1352 - source: new-foo-content 1353 target: / 1354 - name: bare-one 1355 type: bare 1356 size: 1M 1357 content: 1358 - image: new-bare-content.img 1359 ` 1360 1361 s.state.Lock() 1362 s.state.Set("seeded", true) 1363 s.state.Set("refresh-privacy-key", "some-privacy-key") 1364 1365 s.mockTasksNopHandler("fake-download", "validate-snap", "set-model") 1366 1367 // set a model assertion we remodel from 1368 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 1369 "architecture": "amd64", 1370 "kernel": "pc-kernel", 1371 "gadget": "pc", 1372 "base": "core18", 1373 }) 1374 s.makeSerialAssertionInState(c, "canonical", "pc-model", "serial") 1375 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 1376 Brand: "canonical", 1377 Model: "pc-model", 1378 Serial: "serial", 1379 }) 1380 1381 // the target model 1382 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 1383 "architecture": "amd64", 1384 "kernel": "pc-kernel", 1385 "base": "core18", 1386 "revision": "1", 1387 // remodel to new gadget 1388 "gadget": "new-gadget", 1389 }) 1390 1391 // current gadget 1392 siModelGadget := &snap.SideInfo{ 1393 RealName: "pc", 1394 Revision: snap.R(33), 1395 SnapID: "foo-id", 1396 } 1397 currentGadgetInfo := snaptest.MockSnapWithFiles(c, snapYaml, siModelGadget, [][]string{ 1398 {"meta/gadget.yaml", currentGadgetYaml}, 1399 }) 1400 snapstate.Set(s.state, "pc", &snapstate.SnapState{ 1401 SnapType: "gadget", 1402 Sequence: []*snap.SideInfo{siModelGadget}, 1403 Current: siModelGadget.Revision, 1404 Active: true, 1405 }) 1406 1407 // new gadget snap 1408 siNewModelGadget := &snap.SideInfo{ 1409 RealName: "new-gadget", 1410 Revision: snap.R(34), 1411 } 1412 newGadgetInfo := snaptest.MockSnapWithFiles(c, snapYaml, siNewModelGadget, [][]string{ 1413 {"meta/gadget.yaml", remodelGadgetYaml}, 1414 }) 1415 1416 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) { 1417 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 1418 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 1419 tValidate.WaitFor(tDownload) 1420 tGadgetUpdate := s.state.NewTask("update-gadget-assets", fmt.Sprintf("Update gadget %s", name)) 1421 tGadgetUpdate.Set("snap-setup", &snapstate.SnapSetup{ 1422 SideInfo: siNewModelGadget, 1423 Type: snap.TypeGadget, 1424 }) 1425 tGadgetUpdate.WaitFor(tValidate) 1426 ts := state.NewTaskSet(tDownload, tValidate, tGadgetUpdate) 1427 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 1428 return ts, nil 1429 }) 1430 defer restore() 1431 restore = release.MockOnClassic(false) 1432 defer restore() 1433 1434 gadgetUpdateCalled := false 1435 restore = devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 1436 gadgetUpdateCalled = true 1437 c.Check(policy, NotNil) 1438 c.Check(reflect.ValueOf(policy).Pointer(), Equals, reflect.ValueOf(gadget.RemodelUpdatePolicy).Pointer()) 1439 c.Check(current, DeepEquals, gadget.GadgetData{ 1440 Info: &gadget.Info{ 1441 Volumes: map[string]*gadget.Volume{ 1442 "pc": { 1443 Bootloader: "grub", 1444 Schema: "gpt", 1445 Structure: []gadget.VolumeStructure{{ 1446 Name: "foo", 1447 Type: "00000000-0000-0000-0000-0000deadcafe", 1448 Size: 10 * quantity.SizeMiB, 1449 Filesystem: "ext4", 1450 Content: []gadget.VolumeContent{ 1451 {UnresolvedSource: "foo-content", Target: "/"}, 1452 }, 1453 }, { 1454 Name: "bare-one", 1455 Type: "bare", 1456 Size: quantity.SizeMiB, 1457 Content: []gadget.VolumeContent{ 1458 {Image: "bare.img"}, 1459 }, 1460 }}, 1461 }, 1462 }, 1463 }, 1464 RootDir: currentGadgetInfo.MountDir(), 1465 }) 1466 c.Check(update, DeepEquals, gadget.GadgetData{ 1467 Info: &gadget.Info{ 1468 Volumes: map[string]*gadget.Volume{ 1469 "pc": { 1470 Bootloader: "grub", 1471 Schema: "gpt", 1472 Structure: []gadget.VolumeStructure{{ 1473 Name: "foo", 1474 Type: "00000000-0000-0000-0000-0000deadcafe", 1475 Size: 10 * quantity.SizeMiB, 1476 Filesystem: "ext4", 1477 Content: []gadget.VolumeContent{ 1478 {UnresolvedSource: "new-foo-content", Target: "/"}, 1479 }, 1480 }, { 1481 Name: "bare-one", 1482 Type: "bare", 1483 Size: quantity.SizeMiB, 1484 Content: []gadget.VolumeContent{ 1485 {Image: "new-bare-content.img"}, 1486 }, 1487 }}, 1488 }, 1489 }, 1490 }, 1491 RootDir: newGadgetInfo.MountDir(), 1492 }) 1493 return nil 1494 }) 1495 defer restore() 1496 1497 chg, err := devicestate.Remodel(s.state, new) 1498 c.Assert(err, IsNil) 1499 s.state.Unlock() 1500 1501 s.settle(c) 1502 1503 s.state.Lock() 1504 defer s.state.Unlock() 1505 c.Check(chg.IsReady(), Equals, true) 1506 c.Check(chg.Err(), IsNil) 1507 c.Check(gadgetUpdateCalled, Equals, true) 1508 c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystem}) 1509 } 1510 1511 func (s *deviceMgrRemodelSuite) TestRemodelGadgetAssetsParanoidCheck(c *C) { 1512 s.state.Lock() 1513 s.state.Set("seeded", true) 1514 s.state.Set("refresh-privacy-key", "some-privacy-key") 1515 1516 s.mockTasksNopHandler("fake-download", "validate-snap", "set-model") 1517 1518 // set a model assertion we remodel from 1519 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 1520 "architecture": "amd64", 1521 "kernel": "pc-kernel", 1522 "gadget": "pc", 1523 "base": "core18", 1524 }) 1525 s.makeSerialAssertionInState(c, "canonical", "pc-model", "serial") 1526 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 1527 Brand: "canonical", 1528 Model: "pc-model", 1529 Serial: "serial", 1530 }) 1531 1532 // the target model 1533 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 1534 "architecture": "amd64", 1535 "kernel": "pc-kernel", 1536 "base": "core18", 1537 "revision": "1", 1538 // remodel to new gadget 1539 "gadget": "new-gadget", 1540 }) 1541 1542 // current gadget 1543 siModelGadget := &snap.SideInfo{ 1544 RealName: "pc", 1545 Revision: snap.R(33), 1546 SnapID: "foo-id", 1547 } 1548 snapstate.Set(s.state, "pc", &snapstate.SnapState{ 1549 SnapType: "gadget", 1550 Sequence: []*snap.SideInfo{siModelGadget}, 1551 Current: siModelGadget.Revision, 1552 Active: true, 1553 }) 1554 1555 // new gadget snap, name does not match the new model 1556 siUnexpectedModelGadget := &snap.SideInfo{ 1557 RealName: "new-gadget-unexpected", 1558 Revision: snap.R(34), 1559 } 1560 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) { 1561 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 1562 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 1563 tValidate.WaitFor(tDownload) 1564 tGadgetUpdate := s.state.NewTask("update-gadget-assets", fmt.Sprintf("Update gadget %s", name)) 1565 tGadgetUpdate.Set("snap-setup", &snapstate.SnapSetup{ 1566 SideInfo: siUnexpectedModelGadget, 1567 Type: snap.TypeGadget, 1568 }) 1569 tGadgetUpdate.WaitFor(tValidate) 1570 ts := state.NewTaskSet(tDownload, tValidate, tGadgetUpdate) 1571 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 1572 return ts, nil 1573 }) 1574 defer restore() 1575 restore = release.MockOnClassic(false) 1576 defer restore() 1577 1578 gadgetUpdateCalled := false 1579 restore = devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 1580 return errors.New("unexpected call") 1581 }) 1582 defer restore() 1583 1584 chg, err := devicestate.Remodel(s.state, new) 1585 c.Assert(err, IsNil) 1586 s.state.Unlock() 1587 1588 s.settle(c) 1589 1590 s.state.Lock() 1591 defer s.state.Unlock() 1592 c.Check(chg.IsReady(), Equals, true) 1593 c.Assert(chg.Err(), ErrorMatches, `(?s).*\(cannot apply gadget assets update from non-model gadget snap "new-gadget-unexpected", expected "new-gadget" snap\)`) 1594 c.Check(gadgetUpdateCalled, Equals, false) 1595 c.Check(s.restartRequests, HasLen, 0) 1596 } 1597 1598 func (s *deviceMgrSuite) TestRemodelSwitchBase(c *C) { 1599 s.state.Lock() 1600 defer s.state.Unlock() 1601 s.state.Set("seeded", true) 1602 s.state.Set("refresh-privacy-key", "some-privacy-key") 1603 1604 var testDeviceCtx snapstate.DeviceContext 1605 1606 var snapstateInstallWithDeviceContextCalled int 1607 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) { 1608 snapstateInstallWithDeviceContextCalled++ 1609 c.Check(name, Equals, "core20") 1610 1611 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 1612 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 1613 tValidate.WaitFor(tDownload) 1614 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 1615 tInstall.WaitFor(tValidate) 1616 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 1617 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 1618 return ts, nil 1619 }) 1620 defer restore() 1621 1622 // set a model assertion 1623 current := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 1624 "architecture": "amd64", 1625 "kernel": "pc-kernel", 1626 "gadget": "pc", 1627 "base": "core18", 1628 }) 1629 err := assertstate.Add(s.state, current) 1630 c.Assert(err, IsNil) 1631 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 1632 Brand: "canonical", 1633 Model: "pc-model", 1634 }) 1635 1636 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 1637 "architecture": "amd64", 1638 "kernel": "pc-kernel", 1639 "gadget": "pc", 1640 "base": "core20", 1641 "revision": "1", 1642 }) 1643 1644 testDeviceCtx = &snapstatetest.TrivialDeviceContext{Remodeling: true} 1645 1646 tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99") 1647 c.Assert(err, IsNil) 1648 // 1 switch to a new base plus the remodel task 1649 c.Assert(tss, HasLen, 2) 1650 // API was hit 1651 c.Assert(snapstateInstallWithDeviceContextCalled, Equals, 1) 1652 } 1653 1654 func (s *deviceMgrRemodelSuite) TestRemodelUC20RequiredSnapsAndRecoverySystem(c *C) { 1655 s.state.Lock() 1656 defer s.state.Unlock() 1657 s.state.Set("seeded", true) 1658 s.state.Set("refresh-privacy-key", "some-privacy-key") 1659 1660 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) { 1661 c.Check(flags.Required, Equals, true) 1662 c.Check(deviceCtx, NotNil) 1663 c.Check(deviceCtx.ForRemodeling(), Equals, true) 1664 1665 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 1666 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 1667 tValidate.WaitFor(tDownload) 1668 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 1669 tInstall.WaitFor(tValidate) 1670 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 1671 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 1672 return ts, nil 1673 }) 1674 defer restore() 1675 1676 now := time.Now() 1677 restore = devicestate.MockTimeNow(func() time.Time { return now }) 1678 defer restore() 1679 1680 restore = devicestate.AllowUC20RemodelTesting(true) 1681 defer restore() 1682 1683 // set a model assertion 1684 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 1685 "architecture": "amd64", 1686 "base": "core20", 1687 "grade": "dangerous", 1688 "snaps": []interface{}{ 1689 map[string]interface{}{ 1690 "name": "pc-kernel", 1691 "id": snaptest.AssertedSnapID("pc-kernel"), 1692 "type": "kernel", 1693 "default-channel": "20", 1694 }, 1695 map[string]interface{}{ 1696 "name": "pc", 1697 "id": snaptest.AssertedSnapID("pc"), 1698 "type": "gadget", 1699 "default-channel": "20", 1700 }, 1701 }, 1702 }) 1703 s.makeSerialAssertionInState(c, "canonical", "pc-model", "serial") 1704 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 1705 Brand: "canonical", 1706 Model: "pc-model", 1707 Serial: "serial", 1708 }) 1709 1710 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 1711 "architecture": "amd64", 1712 "base": "core20", 1713 "grade": "dangerous", 1714 "revision": "1", 1715 "snaps": []interface{}{ 1716 map[string]interface{}{ 1717 "name": "pc-kernel", 1718 "id": snaptest.AssertedSnapID("pc-kernel"), 1719 "type": "kernel", 1720 "default-channel": "20", 1721 }, 1722 map[string]interface{}{ 1723 "name": "pc", 1724 "id": snaptest.AssertedSnapID("pc"), 1725 "type": "gadget", 1726 "default-channel": "20", 1727 }, 1728 map[string]interface{}{ 1729 "name": "new-required-snap-1", 1730 "id": snaptest.AssertedSnapID("new-required-snap-1"), 1731 "presence": "required", 1732 }, 1733 map[string]interface{}{ 1734 "name": "new-required-snap-2", 1735 "id": snaptest.AssertedSnapID("new-required-snap-2"), 1736 "presence": "required", 1737 }, 1738 }, 1739 }) 1740 chg, err := devicestate.Remodel(s.state, new) 1741 c.Assert(err, IsNil) 1742 c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") 1743 1744 tl := chg.Tasks() 1745 // 2 snaps (3 tasks for each) + recovery system (2 tasks) + set-model 1746 c.Assert(tl, HasLen, 2*3+2+1) 1747 1748 deviceCtx, err := devicestate.DeviceCtx(s.state, tl[0], nil) 1749 c.Assert(err, IsNil) 1750 // deviceCtx is actually a remodelContext here 1751 remodCtx, ok := deviceCtx.(devicestate.RemodelContext) 1752 c.Assert(ok, Equals, true) 1753 c.Check(remodCtx.ForRemodeling(), Equals, true) 1754 c.Check(remodCtx.Kind(), Equals, devicestate.UpdateRemodel) 1755 c.Check(remodCtx.Model(), DeepEquals, new) 1756 c.Check(remodCtx.Store(), IsNil) 1757 1758 // check the tasks 1759 tDownloadSnap1 := tl[0] 1760 tValidateSnap1 := tl[1] 1761 tInstallSnap1 := tl[2] 1762 tDownloadSnap2 := tl[3] 1763 tValidateSnap2 := tl[4] 1764 tInstallSnap2 := tl[5] 1765 tCreateRecovery := tl[6] 1766 tFinalizeRecovery := tl[7] 1767 tSetModel := tl[8] 1768 1769 // check the tasks 1770 c.Assert(tDownloadSnap1.Kind(), Equals, "fake-download") 1771 c.Assert(tDownloadSnap1.Summary(), Equals, "Download new-required-snap-1") 1772 c.Assert(tDownloadSnap1.WaitTasks(), HasLen, 0) 1773 c.Assert(tValidateSnap1.Kind(), Equals, "validate-snap") 1774 c.Assert(tValidateSnap1.Summary(), Equals, "Validate new-required-snap-1") 1775 expectedLabel := now.Format("20060102") 1776 c.Assert(tCreateRecovery.Kind(), Equals, "create-recovery-system") 1777 c.Assert(tCreateRecovery.Summary(), Equals, fmt.Sprintf("Create recovery system with label %q", expectedLabel)) 1778 c.Assert(tFinalizeRecovery.Kind(), Equals, "finalize-recovery-system") 1779 c.Assert(tFinalizeRecovery.Summary(), Equals, fmt.Sprintf("Finalize recovery system with label %q", expectedLabel)) 1780 // check the ordering, download/validate everything first, then install 1781 1782 // snap2 downloads wait for the downloads of snap1 1783 c.Assert(tDownloadSnap1.WaitTasks(), HasLen, 0) 1784 c.Assert(tValidateSnap1.WaitTasks(), DeepEquals, []*state.Task{ 1785 tDownloadSnap1, 1786 }) 1787 c.Assert(tInstallSnap1.WaitTasks(), DeepEquals, []*state.Task{ 1788 tValidateSnap1, 1789 tValidateSnap2, 1790 // wait for recovery system to be created 1791 tCreateRecovery, 1792 // and then finalized 1793 tFinalizeRecovery, 1794 }) 1795 c.Assert(tInstallSnap2.WaitTasks(), DeepEquals, []*state.Task{ 1796 tValidateSnap2, 1797 // previous install chain 1798 tInstallSnap1, 1799 }) 1800 c.Assert(tCreateRecovery.WaitTasks(), DeepEquals, []*state.Task{ 1801 // last snap of the download chain 1802 tValidateSnap2, 1803 }) 1804 c.Assert(tFinalizeRecovery.WaitTasks(), DeepEquals, []*state.Task{ 1805 // recovery system being created 1806 tCreateRecovery, 1807 // last snap of the download chain (added later) 1808 tValidateSnap2, 1809 }) 1810 1811 c.Assert(tSetModel.Kind(), Equals, "set-model") 1812 c.Assert(tSetModel.Summary(), Equals, "Set new model assertion") 1813 // setModel waits for everything in the change 1814 c.Assert(tSetModel.WaitTasks(), DeepEquals, []*state.Task{ 1815 tDownloadSnap1, tValidateSnap1, tInstallSnap1, 1816 tDownloadSnap2, tValidateSnap2, tInstallSnap2, 1817 tCreateRecovery, tFinalizeRecovery, 1818 }) 1819 1820 // verify recovery system setup data on appropriate tasks 1821 var systemSetupData map[string]interface{} 1822 err = tCreateRecovery.Get("recovery-system-setup", &systemSetupData) 1823 c.Assert(err, IsNil) 1824 c.Assert(systemSetupData, DeepEquals, map[string]interface{}{ 1825 "label": expectedLabel, 1826 "directory": filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", expectedLabel), 1827 "snap-setup-tasks": []interface{}{tDownloadSnap1.ID(), tDownloadSnap2.ID()}, 1828 }) 1829 // cross references of to recovery system setup data 1830 for _, tsk := range []*state.Task{tFinalizeRecovery, tSetModel} { 1831 var otherTaskID string 1832 // finalize-recovery-system points to create-recovery-system 1833 err = tsk.Get("recovery-system-setup-task", &otherTaskID) 1834 c.Assert(err, IsNil, Commentf("recovery system setup task ID missing in %s", tsk.Kind())) 1835 c.Assert(otherTaskID, Equals, tCreateRecovery.ID()) 1836 } 1837 } 1838 1839 type remodelUC20LabelConflictsTestCase struct { 1840 now time.Time 1841 breakPermissions bool 1842 expectedErr string 1843 } 1844 1845 func (s *deviceMgrRemodelSuite) testRemodelUC20LabelConflicts(c *C, tc remodelUC20LabelConflictsTestCase) { 1846 restore := devicestate.AllowUC20RemodelTesting(true) 1847 defer restore() 1848 1849 s.state.Lock() 1850 defer s.state.Unlock() 1851 s.state.Set("seeded", true) 1852 s.state.Set("refresh-privacy-key", "some-privacy-key") 1853 1854 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) { 1855 return nil, fmt.Errorf("unexpected call") 1856 }) 1857 defer restore() 1858 1859 restore = devicestate.MockTimeNow(func() time.Time { return tc.now }) 1860 defer restore() 1861 1862 // set a model assertion 1863 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 1864 "architecture": "amd64", 1865 "base": "core20", 1866 "grade": "dangerous", 1867 "snaps": []interface{}{ 1868 map[string]interface{}{ 1869 "name": "pc-kernel", 1870 "id": snaptest.AssertedSnapID("pc-kernel"), 1871 "type": "kernel", 1872 "default-channel": "20", 1873 }, 1874 map[string]interface{}{ 1875 "name": "pc", 1876 "id": snaptest.AssertedSnapID("pc"), 1877 "type": "gadget", 1878 "default-channel": "20", 1879 }, 1880 }, 1881 }) 1882 s.makeSerialAssertionInState(c, "canonical", "pc-model", "serial") 1883 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 1884 Brand: "canonical", 1885 Model: "pc-model", 1886 Serial: "serial", 1887 }) 1888 1889 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 1890 "architecture": "amd64", 1891 "base": "core20", 1892 "grade": "dangerous", 1893 "revision": "1", 1894 "snaps": []interface{}{ 1895 map[string]interface{}{ 1896 "name": "pc-kernel", 1897 "id": snaptest.AssertedSnapID("pc-kernel"), 1898 "type": "kernel", 1899 "default-channel": "20", 1900 }, 1901 map[string]interface{}{ 1902 "name": "pc", 1903 "id": snaptest.AssertedSnapID("pc"), 1904 "type": "gadget", 1905 "default-channel": "20", 1906 }, 1907 }, 1908 }) 1909 1910 labelBase := tc.now.Format("20060102") 1911 // create a conflict with base label 1912 err := os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", labelBase), 0755) 1913 c.Assert(err, IsNil) 1914 for i := 0; i < 5; i++ { 1915 // create conflicting labels with numerical suffices 1916 l := fmt.Sprintf("%s-%d", labelBase, i) 1917 err := os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", l), 0755) 1918 c.Assert(err, IsNil) 1919 } 1920 // and some confusing labels 1921 for _, suffix := range []string{"--", "-abc", "-abc-1", "foo", "-"} { 1922 err := os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", labelBase+suffix), 0755) 1923 c.Assert(err, IsNil) 1924 } 1925 // and a label that will force a max number 1926 err = os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", labelBase+"-990"), 0755) 1927 c.Assert(err, IsNil) 1928 1929 if tc.breakPermissions { 1930 systemsDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems") 1931 c.Assert(os.Chmod(systemsDir, 0000), IsNil) 1932 defer os.Chmod(systemsDir, 0755) 1933 } 1934 1935 chg, err := devicestate.Remodel(s.state, new) 1936 if tc.expectedErr == "" { 1937 c.Assert(err, IsNil) 1938 c.Assert(chg, NotNil) 1939 1940 var tCreateRecovery *state.Task 1941 for _, tsk := range chg.Tasks() { 1942 if tsk.Kind() == "create-recovery-system" { 1943 tCreateRecovery = tsk 1944 break 1945 } 1946 } 1947 happyLabel := labelBase + "-991" 1948 c.Assert(tCreateRecovery, NotNil) 1949 c.Assert(tCreateRecovery.Summary(), Equals, fmt.Sprintf("Create recovery system with label %q", happyLabel)) 1950 var systemSetupData map[string]interface{} 1951 err = tCreateRecovery.Get("recovery-system-setup", &systemSetupData) 1952 c.Assert(err, IsNil) 1953 c.Assert(systemSetupData["label"], Equals, happyLabel) 1954 } else { 1955 c.Assert(err, ErrorMatches, tc.expectedErr) 1956 c.Assert(chg, IsNil) 1957 } 1958 } 1959 1960 func (s *deviceMgrRemodelSuite) TestRemodelUC20LabelConflictsHappy(c *C) { 1961 now := time.Now() 1962 s.testRemodelUC20LabelConflicts(c, remodelUC20LabelConflictsTestCase{now: now}) 1963 } 1964 1965 func (s *deviceMgrRemodelSuite) TestRemodelUC20LabelConflictsError(c *C) { 1966 if os.Geteuid() == 0 { 1967 c.Skip("the test cannot be executed by the root user") 1968 } 1969 now := time.Now() 1970 nowLabel := now.Format("20060102") 1971 s.testRemodelUC20LabelConflicts(c, remodelUC20LabelConflictsTestCase{ 1972 now: now, 1973 1974 breakPermissions: true, 1975 expectedErr: fmt.Sprintf(`cannot select non-conflicting label for recovery system "%[1]s": stat .*/run/mnt/ubuntu-seed/systems/%[1]s: permission denied`, nowLabel), 1976 }) 1977 } 1978 1979 type uc20RemodelSetModelTestCase struct { 1980 // errors on consecutive reseals 1981 resealErr []error 1982 taskLogMatch string 1983 logMatch string 1984 } 1985 1986 func (s *deviceMgrRemodelSuite) testUC20RemodelSetModel(c *C, tc uc20RemodelSetModelTestCase) { 1987 s.state.Lock() 1988 defer s.state.Unlock() 1989 s.state.Set("seeded", true) 1990 s.state.Set("refresh-privacy-key", "some-privacy-key") 1991 1992 devicestate.SetBootOkRan(s.mgr, true) 1993 1994 c.Assert(os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir, "device"), 0755), IsNil) 1995 1996 s.mockTasksNopHandler("fake-download", "validate-snap", "fake-install", 1997 // create recovery system requests are boot, which is not done here 1998 "create-recovery-system", "finalize-recovery-system") 1999 2000 // set a model assertion we remodel from 2001 model := s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 2002 "architecture": "amd64", 2003 "base": "core20", 2004 "grade": "dangerous", 2005 "snaps": []interface{}{ 2006 map[string]interface{}{ 2007 "name": "pc-kernel", 2008 "id": snaptest.AssertedSnapID("pc-kernel"), 2009 "type": "kernel", 2010 "default-channel": "20", 2011 }, 2012 map[string]interface{}{ 2013 "name": "pc", 2014 "id": snaptest.AssertedSnapID("pc"), 2015 "type": "gadget", 2016 "default-channel": "20", 2017 }, 2018 }, 2019 }) 2020 s.makeSerialAssertionInState(c, "canonical", "pc-model", "serial") 2021 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 2022 Brand: "canonical", 2023 Model: "pc-model", 2024 Serial: "serial", 2025 }) 2026 2027 oldSeededTs := time.Now().AddDate(0, 0, -1) 2028 s.state.Set("seeded-systems", []devicestate.SeededSystem{ 2029 { 2030 System: "0000", 2031 Model: model.Model(), 2032 BrandID: model.BrandID(), 2033 Timestamp: model.Timestamp(), 2034 SeedTime: oldSeededTs, 2035 }, 2036 }) 2037 2038 // the target model 2039 new := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 2040 "architecture": "amd64", 2041 "base": "core20", 2042 "grade": "dangerous", 2043 "revision": "1", 2044 "snaps": []interface{}{ 2045 map[string]interface{}{ 2046 "name": "pc-kernel", 2047 "id": snaptest.AssertedSnapID("pc-kernel"), 2048 "type": "kernel", 2049 "default-channel": "20", 2050 }, 2051 map[string]interface{}{ 2052 "name": "pc", 2053 "id": snaptest.AssertedSnapID("pc"), 2054 "type": "gadget", 2055 "default-channel": "20", 2056 }, 2057 }, 2058 }) 2059 2060 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) { 2061 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 2062 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 2063 tValidate.WaitFor(tDownload) 2064 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 2065 tInstall.WaitFor(tValidate) 2066 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 2067 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 2068 return ts, nil 2069 }) 2070 defer restore() 2071 restore = release.MockOnClassic(false) 2072 defer restore() 2073 2074 buf, restore := logger.MockLogger() 2075 defer restore() 2076 2077 restore = devicestate.AllowUC20RemodelTesting(true) 2078 defer restore() 2079 2080 m := boot.Modeenv{ 2081 Mode: "run", 2082 2083 GoodRecoverySystems: []string{"0000"}, 2084 CurrentRecoverySystems: []string{"0000"}, 2085 2086 Model: model.Model(), 2087 BrandID: model.BrandID(), 2088 Grade: string(model.Grade()), 2089 ModelSignKeyID: model.SignKeyID(), 2090 } 2091 c.Assert(m.WriteTo(""), IsNil) 2092 2093 now := time.Now() 2094 expectedLabel := now.Format("20060102") 2095 restore = devicestate.MockTimeNow(func() time.Time { return now }) 2096 defer restore() 2097 s.state.Set("tried-systems", []string{expectedLabel}) 2098 2099 resealKeyCalls := 0 2100 restore = boot.MockResealKeyToModeenv(func(rootdir string, modeenv *boot.Modeenv, expectReseal bool) error { 2101 resealKeyCalls++ 2102 c.Assert(len(tc.resealErr) >= resealKeyCalls, Equals, true) 2103 c.Check(modeenv.GoodRecoverySystems, DeepEquals, []string{"0000", expectedLabel}) 2104 c.Check(modeenv.CurrentRecoverySystems, DeepEquals, []string{"0000", expectedLabel}) 2105 return tc.resealErr[resealKeyCalls-1] 2106 }) 2107 defer restore() 2108 2109 chg, err := devicestate.Remodel(s.state, new) 2110 c.Assert(err, IsNil) 2111 var setModelTask *state.Task 2112 for _, tsk := range chg.Tasks() { 2113 if tsk.Kind() == "set-model" { 2114 setModelTask = tsk 2115 break 2116 } 2117 } 2118 s.state.Unlock() 2119 2120 s.settle(c) 2121 2122 s.state.Lock() 2123 c.Check(chg.IsReady(), Equals, true) 2124 c.Check(chg.Err(), IsNil) 2125 c.Check(resealKeyCalls, Equals, len(tc.resealErr)) 2126 // even if errors occur during reseal, set-model is a point of no return 2127 c.Check(setModelTask.Status(), Equals, state.DoneStatus) 2128 var seededSystems []devicestate.SeededSystem 2129 c.Assert(s.state.Get("seeded-systems", &seededSystems), IsNil) 2130 hasError := false 2131 for _, err := range tc.resealErr { 2132 if err != nil { 2133 hasError = true 2134 break 2135 } 2136 } 2137 if !hasError { 2138 c.Check(setModelTask.Log(), HasLen, 0) 2139 2140 c.Assert(seededSystems, HasLen, 2) 2141 // the system was seeded after our mocked 'now' or at the same 2142 // time if clock resolution is very low, but not before it 2143 c.Check(seededSystems[0].SeedTime.Before(now), Equals, false) 2144 seededSystems[0].SeedTime = time.Time{} 2145 c.Check(seededSystems[1].SeedTime.Equal(oldSeededTs), Equals, true) 2146 seededSystems[1].SeedTime = time.Time{} 2147 c.Check(seededSystems, DeepEquals, []devicestate.SeededSystem{ 2148 { 2149 System: expectedLabel, 2150 Model: new.Model(), 2151 BrandID: new.BrandID(), 2152 Revision: new.Revision(), 2153 Timestamp: new.Timestamp(), 2154 }, 2155 { 2156 System: "0000", 2157 Model: model.Model(), 2158 BrandID: model.BrandID(), 2159 Timestamp: model.Timestamp(), 2160 Revision: model.Revision(), 2161 }, 2162 }) 2163 } else { 2164 // however, error is still logged, both to the task and the logger 2165 c.Check(strings.Join(setModelTask.Log(), "\n"), Matches, tc.taskLogMatch) 2166 c.Check(buf.String(), Matches, tc.logMatch) 2167 2168 c.Assert(seededSystems, HasLen, 1) 2169 c.Check(seededSystems[0].SeedTime.Equal(oldSeededTs), Equals, true) 2170 seededSystems[0].SeedTime = time.Time{} 2171 c.Check(seededSystems, DeepEquals, []devicestate.SeededSystem{ 2172 { 2173 System: "0000", 2174 Model: model.Model(), 2175 BrandID: model.BrandID(), 2176 Timestamp: model.Timestamp(), 2177 Revision: model.Revision(), 2178 }, 2179 }) 2180 } 2181 } 2182 2183 func (s *deviceMgrRemodelSuite) TestUC20RemodelSetModelHappy(c *C) { 2184 s.testUC20RemodelSetModel(c, uc20RemodelSetModelTestCase{ 2185 resealErr: []error{ 2186 nil, // promote recovery system 2187 nil, // device change pre model write 2188 nil, // device change post model write 2189 }, 2190 }) 2191 } 2192 2193 func (s *deviceMgrRemodelSuite) TestUC20RemodelSetModelErr(c *C) { 2194 s.testUC20RemodelSetModel(c, uc20RemodelSetModelTestCase{ 2195 resealErr: []error{ 2196 nil, // promote tried recovery system 2197 // keep this comment so that gofmt does not complain 2198 fmt.Errorf("mock reseal error"), // device change pre model write 2199 }, 2200 taskLogMatch: `.* cannot complete remodel: \[cannot switch device: mock reseal error\]`, 2201 logMatch: `(?s).* cannot complete remodel: \[cannot switch device: mock reseal error\].`, 2202 }) 2203 } 2204 2205 func (s *deviceMgrRemodelSuite) TestUC20RemodelSetModelWithReboot(c *C) { 2206 // check that set-model does the right thing even if it is restarted 2207 // after an unexpected reboot; this gets complicated as we cannot 2208 // panic() at a random place in the task runner, so we set up the state 2209 // such that the set-model task completes once and is re-run again 2210 2211 s.state.Lock() 2212 defer s.state.Unlock() 2213 s.state.Set("seeded", true) 2214 s.state.Set("refresh-privacy-key", "some-privacy-key") 2215 2216 devicestate.SetBootOkRan(s.mgr, true) 2217 2218 s.newFakeStore = func(devBE storecontext.DeviceBackend) snapstate.StoreService { 2219 return &freshSessionStore{} 2220 } 2221 2222 s.mockTasksNopHandler("fake-download", "validate-snap", "fake-install", 2223 "check-snap", "request-serial", 2224 // create recovery system requests are boot, which is not done 2225 // here 2226 "create-recovery-system", "finalize-recovery-system") 2227 2228 // set a model assertion we remodel from 2229 model := s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 2230 "architecture": "amd64", 2231 "base": "core20", 2232 "grade": "dangerous", 2233 "snaps": []interface{}{ 2234 map[string]interface{}{ 2235 "name": "pc-kernel", 2236 "id": snaptest.AssertedSnapID("pc-kernel"), 2237 "type": "kernel", 2238 "default-channel": "20", 2239 }, 2240 map[string]interface{}{ 2241 "name": "pc", 2242 "id": snaptest.AssertedSnapID("pc"), 2243 "type": "gadget", 2244 "default-channel": "20", 2245 }, 2246 }, 2247 }) 2248 writeDeviceModelToUbuntuBoot(c, model) 2249 // the gadget needs to be mocked 2250 info := snaptest.MakeSnapFileAndDir(c, "name: pc\nversion: 1\ntype: gadget\n", nil, &snap.SideInfo{ 2251 SnapID: snaptest.AssertedSnapID("pc"), 2252 Revision: snap.R(1), 2253 RealName: "pc", 2254 }) 2255 snapstate.Set(s.state, info.InstanceName(), &snapstate.SnapState{ 2256 SnapType: string(info.Type()), 2257 Active: true, 2258 Sequence: []*snap.SideInfo{&info.SideInfo}, 2259 Current: info.Revision, 2260 }) 2261 2262 s.makeSerialAssertionInState(c, "canonical", "pc-model", "serial") 2263 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 2264 Brand: "canonical", 2265 Model: "pc-model", 2266 Serial: "serial", 2267 }) 2268 oldSeededTs := time.Now().AddDate(0, 0, -1) 2269 s.state.Set("seeded-systems", []devicestate.SeededSystem{ 2270 { 2271 System: "0000", 2272 Model: model.Model(), 2273 BrandID: model.BrandID(), 2274 Timestamp: model.Timestamp(), 2275 SeedTime: oldSeededTs, 2276 }, 2277 }) 2278 2279 // the target model, since it's a new model altogether a reregistration 2280 // will be triggered 2281 new := s.brands.Model("canonical", "pc-new-model", map[string]interface{}{ 2282 "architecture": "amd64", 2283 "base": "core20", 2284 "grade": "dangerous", 2285 "revision": "1", 2286 "snaps": []interface{}{ 2287 map[string]interface{}{ 2288 "name": "pc-kernel", 2289 "id": snaptest.AssertedSnapID("pc-kernel"), 2290 "type": "kernel", 2291 "default-channel": "20", 2292 }, 2293 map[string]interface{}{ 2294 "name": "pc", 2295 "id": snaptest.AssertedSnapID("pc"), 2296 "type": "gadget", 2297 "default-channel": "20", 2298 }, 2299 }, 2300 }) 2301 2302 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) { 2303 tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name)) 2304 tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name)) 2305 tValidate.WaitFor(tDownload) 2306 tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) 2307 tInstall.WaitFor(tValidate) 2308 ts := state.NewTaskSet(tDownload, tValidate, tInstall) 2309 ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) 2310 return ts, nil 2311 }) 2312 defer restore() 2313 restore = release.MockOnClassic(false) 2314 defer restore() 2315 2316 restore = devicestate.AllowUC20RemodelTesting(true) 2317 defer restore() 2318 2319 m := boot.Modeenv{ 2320 Mode: "run", 2321 2322 GoodRecoverySystems: []string{"0000"}, 2323 CurrentRecoverySystems: []string{"0000"}, 2324 2325 Model: model.Model(), 2326 BrandID: model.BrandID(), 2327 Grade: string(model.Grade()), 2328 ModelSignKeyID: model.SignKeyID(), 2329 } 2330 c.Assert(m.WriteTo(""), IsNil) 2331 2332 now := time.Now() 2333 restore = devicestate.MockTimeNow(func() time.Time { return now }) 2334 defer restore() 2335 expectedLabel := now.Format("20060102") 2336 s.state.Set("tried-systems", []string{expectedLabel}) 2337 2338 resealKeyCalls := 0 2339 restore = boot.MockResealKeyToModeenv(func(rootdir string, modeenv *boot.Modeenv, expectReseal bool) error { 2340 resealKeyCalls++ 2341 // calls: 2342 // 1 - promote recovery system 2343 // 2 - reseal with both models 2344 // 3 - reseal with new model as current 2345 // (mocked reboot) 2346 // 4 - promote recovery system 2347 // 5 - reseal with new model as current and try; before reboot 2348 // set-model changed the model in the state, the new model 2349 // replaced the old one, and thus the remodel context 2350 // carries the new model in ground context 2351 // 6 - reseal with new model as current 2352 c.Check(modeenv.GoodRecoverySystems, DeepEquals, []string{"0000", expectedLabel}) 2353 c.Check(modeenv.CurrentRecoverySystems, DeepEquals, []string{"0000", expectedLabel}) 2354 switch resealKeyCalls { 2355 case 2: 2356 c.Check(modeenv.Model, Equals, model.Model()) 2357 c.Check(modeenv.TryModel, Equals, new.Model()) 2358 c.Check(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model"), 2359 testutil.FileContains, fmt.Sprintf("model: %s\n", model.Model())) 2360 // old model's revision is 0 2361 c.Check(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model"), 2362 Not(testutil.FileContains), "revision:") 2363 case 3: 2364 c.Check(modeenv.Model, Equals, new.Model()) 2365 c.Check(modeenv.TryModel, Equals, "") 2366 c.Check(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model"), 2367 testutil.FileContains, fmt.Sprintf("model: %s\n", new.Model())) 2368 c.Check(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model"), 2369 testutil.FileContains, fmt.Sprintf("revision: %v\n", new.Revision())) 2370 case 5: 2371 c.Check(modeenv.Model, Equals, model.Model()) 2372 c.Check(modeenv.TryModel, Equals, new.Model()) 2373 // we are in an after reboot scenario, the file contains 2374 // the new model 2375 c.Check(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model"), 2376 testutil.FileContains, fmt.Sprintf("model: %s\n", new.Model())) 2377 c.Check(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model"), 2378 testutil.FileContains, fmt.Sprintf("revision: %v\n", new.Revision())) 2379 case 6: 2380 c.Check(modeenv.Model, Equals, new.Model()) 2381 c.Check(modeenv.TryModel, Equals, "") 2382 c.Check(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model"), 2383 testutil.FileContains, fmt.Sprintf("model: %s\n", new.Model())) 2384 c.Check(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model"), 2385 testutil.FileContains, fmt.Sprintf("revision: %v\n", new.Revision())) 2386 } 2387 if resealKeyCalls > 6 { 2388 c.Fatalf("unexpected #%v call to reseal key to modeenv", resealKeyCalls) 2389 } 2390 return nil 2391 }) 2392 defer restore() 2393 2394 chg, err := devicestate.Remodel(s.state, new) 2395 c.Assert(err, IsNil) 2396 2397 // since we cannot panic in random place in code that runs under 2398 // taskrunner, we reset the task status and retry the change again, but 2399 // we cannot do that once a change has become ready, thus inject a task 2400 // that will request a reboot and keep retrying, thus stopping execution 2401 // and keeping the change in a not ready state 2402 fakeRebootCalls := 0 2403 fakeRebootCallsReady := false 2404 s.o.TaskRunner().AddHandler("fake-reboot-and-stall", func(task *state.Task, _ *tomb.Tomb) error { 2405 fakeRebootCalls++ 2406 if fakeRebootCalls == 1 { 2407 st := task.State() 2408 st.Lock() 2409 defer st.Unlock() 2410 // not strictly needed, but underlines there's a reboot 2411 // happening 2412 st.RequestRestart(state.RestartSystemNow) 2413 } 2414 if fakeRebootCallsReady { 2415 return nil 2416 } 2417 // we're not ready, so that the change does not complete yet 2418 return &state.Retry{} 2419 }, nil) 2420 fakeRebootTask := s.state.NewTask("fake-reboot-and-stall", "fake reboot and stalling injected by tests") 2421 chg.AddTask(fakeRebootTask) 2422 var setModelTask *state.Task 2423 for _, tsk := range chg.Tasks() { 2424 if tsk.Kind() == "set-model" { 2425 c.Fatalf("set-model present too early") 2426 } 2427 // make fake-reboot run after all tasks 2428 if tsk.Kind() != "fake-reboot-and-stall" { 2429 fakeRebootTask.WaitFor(tsk) 2430 } 2431 } 2432 s.state.Unlock() 2433 2434 s.settle(c) 2435 2436 s.state.Lock() 2437 // set model was injected by prepare-remodeling 2438 for _, tsk := range chg.Tasks() { 2439 if tsk.Kind() == "set-model" { 2440 setModelTask = tsk 2441 break 2442 } 2443 } 2444 c.Check(chg.IsReady(), Equals, false) 2445 c.Check(chg.Err(), IsNil) 2446 // injected by fake restart 2447 c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) 2448 // 3 calls: promote tried system, old & new model, just the new model 2449 c.Check(resealKeyCalls, Equals, 3) 2450 // even if errors occur during reseal, set-model is done 2451 c.Check(setModelTask.Status(), Equals, state.DoneStatus) 2452 2453 // reset the set-model state back to do, simulating a task restart after a reboot 2454 setModelTask.SetStatus(state.DoStatus) 2455 2456 // the seeded systems has already been populated 2457 var seededSystems []devicestate.SeededSystem 2458 c.Assert(s.state.Get("seeded-systems", &seededSystems), IsNil) 2459 c.Assert(seededSystems, HasLen, 2) 2460 // we need to be smart about checking seed time, also verify 2461 // timestamps separately to avoid timezone problems 2462 newSeededTs := seededSystems[0].SeedTime 2463 // the system was seeded after our mocked 'now' or at the same 2464 // time if clock resolution is very low, but not before it 2465 c.Check(newSeededTs.Before(now), Equals, false) 2466 seededSystems[0].SeedTime = time.Time{} 2467 c.Check(seededSystems[1].SeedTime.Equal(oldSeededTs), Equals, true) 2468 seededSystems[1].SeedTime = time.Time{} 2469 expectedSeededSystems := []devicestate.SeededSystem{ 2470 { 2471 System: expectedLabel, 2472 Model: new.Model(), 2473 BrandID: new.BrandID(), 2474 Revision: new.Revision(), 2475 Timestamp: new.Timestamp(), 2476 }, 2477 { 2478 System: "0000", 2479 Model: model.Model(), 2480 BrandID: model.BrandID(), 2481 Timestamp: model.Timestamp(), 2482 Revision: model.Revision(), 2483 }, 2484 } 2485 c.Check(seededSystems, DeepEquals, expectedSeededSystems) 2486 2487 fakeRebootCallsReady = true 2488 // now redo the task again 2489 s.state.Unlock() 2490 2491 s.settle(c) 2492 2493 s.state.Lock() 2494 c.Check(chg.IsReady(), Equals, true) 2495 c.Check(chg.Err(), IsNil) 2496 c.Check(resealKeyCalls, Equals, 6) 2497 c.Check(setModelTask.Status(), Equals, state.DoneStatus) 2498 2499 c.Assert(s.state.Get("seeded-systems", &seededSystems), IsNil) 2500 c.Assert(seededSystems, HasLen, 2) 2501 // seed time should be unchanged 2502 c.Check(seededSystems[0].SeedTime.Equal(newSeededTs), Equals, true) 2503 seededSystems[0].SeedTime = time.Time{} 2504 c.Check(seededSystems[1].SeedTime.Equal(oldSeededTs), Equals, true) 2505 seededSystems[1].SeedTime = time.Time{} 2506 c.Check(seededSystems, DeepEquals, []devicestate.SeededSystem{ 2507 { 2508 System: expectedLabel, 2509 Model: new.Model(), 2510 BrandID: new.BrandID(), 2511 Revision: new.Revision(), 2512 Timestamp: new.Timestamp(), 2513 }, 2514 { 2515 System: "0000", 2516 Model: model.Model(), 2517 BrandID: model.BrandID(), 2518 Timestamp: model.Timestamp(), 2519 Revision: model.Revision(), 2520 }, 2521 }) 2522 }