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