github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/overlord/devicestate/devicestate_gadget_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016-2020 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 "errors" 24 "io/ioutil" 25 "os" 26 "path/filepath" 27 "reflect" 28 "strings" 29 30 . "gopkg.in/check.v1" 31 32 "github.com/snapcore/snapd/asserts" 33 "github.com/snapcore/snapd/boot" 34 "github.com/snapcore/snapd/bootloader" 35 "github.com/snapcore/snapd/bootloader/bootloadertest" 36 "github.com/snapcore/snapd/dirs" 37 "github.com/snapcore/snapd/gadget" 38 "github.com/snapcore/snapd/osutil" 39 "github.com/snapcore/snapd/overlord/auth" 40 "github.com/snapcore/snapd/overlord/devicestate" 41 "github.com/snapcore/snapd/overlord/devicestate/devicestatetest" 42 "github.com/snapcore/snapd/overlord/snapstate" 43 "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" 44 "github.com/snapcore/snapd/overlord/state" 45 "github.com/snapcore/snapd/release" 46 "github.com/snapcore/snapd/snap" 47 "github.com/snapcore/snapd/snap/snaptest" 48 "github.com/snapcore/snapd/testutil" 49 ) 50 51 type deviceMgrGadgetSuite struct { 52 deviceMgrBaseSuite 53 } 54 55 var _ = Suite(&deviceMgrGadgetSuite{}) 56 57 var snapYaml = ` 58 name: foo-gadget 59 type: gadget 60 ` 61 62 var gadgetYaml = ` 63 volumes: 64 pc: 65 bootloader: grub 66 ` 67 68 var uc20gadgetYaml = ` 69 volumes: 70 pc: 71 bootloader: grub 72 structure: 73 - name: ubuntu-seed 74 role: system-seed 75 type: 21686148-6449-6E6F-744E-656564454649 76 size: 20M 77 - name: ubuntu-boot 78 role: system-boot 79 type: 21686148-6449-6E6F-744E-656564454649 80 size: 10M 81 - name: ubuntu-data 82 role: system-data 83 type: 21686148-6449-6E6F-744E-656564454649 84 size: 50M 85 ` 86 87 var uc20gadgetYamlWithSave = uc20gadgetYaml + ` 88 - name: ubuntu-save 89 role: system-save 90 type: 21686148-6449-6E6F-744E-656564454649 91 size: 50M 92 ` 93 94 // this is the kind of volumes setup recommended to be prepared for a possible 95 // UC18 -> UC20 transition 96 var hybridGadgetYaml = ` 97 volumes: 98 hybrid: 99 bootloader: grub 100 structure: 101 - name: mbr 102 type: mbr 103 size: 440 104 content: 105 - image: pc-boot.img 106 - name: BIOS Boot 107 type: DA,21686148-6449-6E6F-744E-656564454649 108 size: 1M 109 offset: 1M 110 offset-write: mbr+92 111 content: 112 - image: pc-core.img 113 - name: EFI System 114 type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B 115 filesystem: vfat 116 filesystem-label: system-boot 117 size: 1200M 118 content: 119 - source: grubx64.efi 120 target: EFI/boot/grubx64.efi 121 - source: shim.efi.signed 122 target: EFI/boot/bootx64.efi 123 - source: mmx64.efi 124 target: EFI/boot/mmx64.efi 125 - source: grub.cfg 126 target: EFI/ubuntu/grub.cfg 127 - name: Ubuntu Boot 128 type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4 129 filesystem: ext4 130 filesystem-label: ubuntu-boot 131 size: 750M 132 ` 133 134 func (s *deviceMgrGadgetSuite) setupModelWithGadget(c *C, gadget string) { 135 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 136 "architecture": "amd64", 137 "kernel": "pc-kernel", 138 "gadget": gadget, 139 "base": "core18", 140 }) 141 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 142 Brand: "canonical", 143 Model: "pc-model", 144 Serial: "serial", 145 }) 146 } 147 148 func (s *deviceMgrGadgetSuite) setupUC20ModelWithGadget(c *C, gadget string) { 149 s.makeModelAssertionInState(c, "canonical", "pc20-model", map[string]interface{}{ 150 "display-name": "UC20 pc model", 151 "architecture": "amd64", 152 "base": "core20", 153 // enough to have a grade set 154 "grade": "dangerous", 155 "snaps": []interface{}{ 156 map[string]interface{}{ 157 "name": "pc-kernel", 158 "id": "pckernelidididididididididididid", 159 "type": "kernel", 160 "default-channel": "20", 161 }, 162 map[string]interface{}{ 163 "name": gadget, 164 "id": "pcididididididididididididididid", 165 "type": "gadget", 166 "default-channel": "20", 167 }}, 168 }) 169 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 170 Brand: "canonical", 171 Model: "pc20-model", 172 Serial: "serial", 173 }) 174 } 175 176 func (s *deviceMgrGadgetSuite) setupGadgetUpdate(c *C, modelGrade, gadgetYamlContent, gadgetYamlContentNext string) (chg *state.Change, tsk *state.Task) { 177 siCurrent := &snap.SideInfo{ 178 RealName: "foo-gadget", 179 Revision: snap.R(33), 180 SnapID: "foo-id", 181 } 182 si := &snap.SideInfo{ 183 RealName: "foo-gadget", 184 Revision: snap.R(34), 185 SnapID: "foo-id", 186 } 187 snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, [][]string{ 188 {"meta/gadget.yaml", gadgetYamlContent}, 189 {"managed-asset", "managed asset rev 33"}, 190 {"trusted-asset", "trusted asset rev 33"}, 191 }) 192 if gadgetYamlContentNext == "" { 193 gadgetYamlContentNext = gadgetYamlContent 194 } 195 snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{ 196 {"meta/gadget.yaml", gadgetYamlContentNext}, 197 {"managed-asset", "managed asset rev 34"}, 198 // SHA3-384: 88478d8afe6925b348b9cd00085f3535959fde7029a64d7841b031acc39415c690796757afab1852a9e09da913a0151b 199 {"trusted-asset", "trusted asset rev 34"}, 200 }) 201 202 s.state.Lock() 203 defer s.state.Unlock() 204 205 if modelGrade == "" { 206 s.setupModelWithGadget(c, "foo-gadget") 207 } else { 208 s.setupUC20ModelWithGadget(c, "foo-gadget") 209 } 210 211 snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{ 212 SnapType: "gadget", 213 Sequence: []*snap.SideInfo{siCurrent}, 214 Current: siCurrent.Revision, 215 Active: true, 216 }) 217 218 tsk = s.state.NewTask("update-gadget-assets", "update gadget") 219 tsk.Set("snap-setup", &snapstate.SnapSetup{ 220 SideInfo: si, 221 Type: snap.TypeGadget, 222 }) 223 chg = s.state.NewChange("dummy", "...") 224 chg.AddTask(tsk) 225 226 return chg, tsk 227 } 228 229 func (s *deviceMgrGadgetSuite) testUpdateGadgetOnCoreSimple(c *C, grade string, encryption bool, gadgetYamlCont, gadgetYamlContNext string) { 230 var updateCalled bool 231 var passedRollbackDir string 232 233 if grade != "" { 234 bootDir := c.MkDir() 235 tbl := bootloadertest.Mock("trusted", bootDir).WithTrustedAssets() 236 tbl.TrustedAssetsList = []string{"trusted-asset"} 237 tbl.ManagedAssetsList = []string{"managed-asset"} 238 bootloader.Force(tbl) 239 defer func() { bootloader.Force(nil) }() 240 } 241 242 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error { 243 updateCalled = true 244 passedRollbackDir = path 245 st, err := os.Stat(path) 246 c.Assert(err, IsNil) 247 m := st.Mode() 248 c.Assert(m.IsDir(), Equals, true) 249 c.Check(m.Perm(), Equals, os.FileMode(0750)) 250 if grade == "" { 251 // non UC20 model 252 c.Check(observer, IsNil) 253 } else { 254 c.Check(observer, NotNil) 255 // expecting a very specific observer 256 trustedUpdateObserver, ok := observer.(*boot.TrustedAssetsUpdateObserver) 257 c.Assert(ok, Equals, true, Commentf("unexpected type: %T", observer)) 258 c.Assert(trustedUpdateObserver, NotNil) 259 260 // check that observer is behaving correctly with 261 // respect to trusted and managed assets 262 targetDir := c.MkDir() 263 sourceStruct := &gadget.LaidOutStructure{ 264 VolumeStructure: &gadget.VolumeStructure{ 265 Role: gadget.SystemSeed, 266 }, 267 } 268 act, err := observer.Observe(gadget.ContentUpdate, sourceStruct, targetDir, "managed-asset", 269 &gadget.ContentChange{After: filepath.Join(update.RootDir, "managed-asset")}) 270 c.Assert(err, IsNil) 271 c.Check(act, Equals, gadget.ChangeIgnore) 272 act, err = observer.Observe(gadget.ContentUpdate, sourceStruct, targetDir, "trusted-asset", 273 &gadget.ContentChange{After: filepath.Join(update.RootDir, "trusted-asset")}) 274 c.Assert(err, IsNil) 275 c.Check(act, Equals, gadget.ChangeApply) 276 // check that the behavior is correct 277 m, err := boot.ReadModeenv("") 278 c.Assert(err, IsNil) 279 if encryption { 280 // with encryption enabled, trusted asset would 281 // have been picked up by the the observer and 282 // added to modenv 283 c.Assert(m.CurrentTrustedRecoveryBootAssets, NotNil) 284 c.Check(m.CurrentTrustedRecoveryBootAssets["trusted-asset"], DeepEquals, 285 []string{"88478d8afe6925b348b9cd00085f3535959fde7029a64d7841b031acc39415c690796757afab1852a9e09da913a0151b"}) 286 } else { 287 c.Check(m.CurrentTrustedRecoveryBootAssets, HasLen, 0) 288 } 289 } 290 return nil 291 }) 292 defer restore() 293 294 chg, t := s.setupGadgetUpdate(c, grade, gadgetYamlCont, gadgetYamlContNext) 295 296 // procure modeenv and stamp that we sealed keys 297 if grade != "" { 298 // state after mark-seeded ran 299 modeenv := boot.Modeenv{ 300 Mode: "run", 301 RecoverySystem: "", 302 } 303 err := modeenv.WriteTo("") 304 c.Assert(err, IsNil) 305 306 if encryption { 307 // sealed keys stamp 308 stamp := filepath.Join(dirs.SnapFDEDir, "sealed-keys") 309 c.Assert(os.MkdirAll(filepath.Dir(stamp), 0755), IsNil) 310 err = ioutil.WriteFile(stamp, nil, 0644) 311 c.Assert(err, IsNil) 312 } 313 } 314 devicestate.SetBootOkRan(s.mgr, true) 315 316 s.state.Lock() 317 s.state.Set("seeded", true) 318 s.state.Unlock() 319 320 s.settle(c) 321 322 s.state.Lock() 323 defer s.state.Unlock() 324 c.Assert(chg.IsReady(), Equals, true) 325 c.Check(chg.Err(), IsNil) 326 c.Check(t.Status(), Equals, state.DoneStatus) 327 c.Check(updateCalled, Equals, true) 328 rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34") 329 c.Check(rollbackDir, Equals, passedRollbackDir) 330 // should have been removed right after update 331 c.Check(osutil.IsDirectory(rollbackDir), Equals, false) 332 c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystem}) 333 } 334 335 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreSimple(c *C) { 336 // unset grade 337 encryption := false 338 s.testUpdateGadgetOnCoreSimple(c, "", encryption, gadgetYaml, "") 339 } 340 341 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnUC20CoreSimpleWithEncryption(c *C) { 342 encryption := true 343 s.testUpdateGadgetOnCoreSimple(c, "dangerous", encryption, uc20gadgetYaml, "") 344 } 345 346 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnUC20CoreSimpleNoEncryption(c *C) { 347 encryption := false 348 s.testUpdateGadgetOnCoreSimple(c, "dangerous", encryption, uc20gadgetYaml, "") 349 } 350 351 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreNoUpdateNeeded(c *C) { 352 var called bool 353 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 354 called = true 355 return gadget.ErrNoUpdate 356 }) 357 defer restore() 358 359 chg, t := s.setupGadgetUpdate(c, "", gadgetYaml, "") 360 361 s.se.Ensure() 362 s.se.Wait() 363 364 s.state.Lock() 365 defer s.state.Unlock() 366 c.Assert(chg.IsReady(), Equals, true) 367 c.Check(chg.Err(), IsNil) 368 c.Check(t.Status(), Equals, state.DoneStatus) 369 c.Check(t.Log(), HasLen, 1) 370 c.Check(t.Log()[0], Matches, ".* INFO No gadget assets update needed") 371 c.Check(called, Equals, true) 372 c.Check(s.restartRequests, HasLen, 0) 373 } 374 375 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreRollbackDirCreateFailed(c *C) { 376 if os.Geteuid() == 0 { 377 c.Skip("this test cannot run as root (permissions are not honored)") 378 } 379 380 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 381 return errors.New("unexpected call") 382 }) 383 defer restore() 384 385 chg, t := s.setupGadgetUpdate(c, "", gadgetYaml, "") 386 387 rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34") 388 err := os.MkdirAll(dirs.SnapRollbackDir, 0000) 389 c.Assert(err, IsNil) 390 391 s.state.Lock() 392 s.state.Set("seeded", true) 393 s.state.Unlock() 394 395 s.settle(c) 396 397 s.state.Lock() 398 defer s.state.Unlock() 399 c.Assert(chg.IsReady(), Equals, true) 400 c.Check(chg.Err(), ErrorMatches, `(?s).*cannot prepare update rollback directory: .*`) 401 c.Check(t.Status(), Equals, state.ErrorStatus) 402 c.Check(osutil.IsDirectory(rollbackDir), Equals, false) 403 c.Check(s.restartRequests, HasLen, 0) 404 } 405 406 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreUpdateFailed(c *C) { 407 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 408 return errors.New("gadget exploded") 409 }) 410 defer restore() 411 chg, t := s.setupGadgetUpdate(c, "", gadgetYaml, "") 412 413 s.state.Lock() 414 s.state.Set("seeded", true) 415 s.state.Unlock() 416 417 s.settle(c) 418 419 s.state.Lock() 420 defer s.state.Unlock() 421 c.Assert(chg.IsReady(), Equals, true) 422 c.Check(chg.Err(), ErrorMatches, `(?s).*update gadget \(gadget exploded\).*`) 423 c.Check(t.Status(), Equals, state.ErrorStatus) 424 rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34") 425 // update rollback left for inspection 426 c.Check(osutil.IsDirectory(rollbackDir), Equals, true) 427 c.Check(s.restartRequests, HasLen, 0) 428 } 429 430 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreNotDuringFirstboot(c *C) { 431 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 432 return errors.New("unexpected call") 433 }) 434 defer restore() 435 436 // simulate first-boot/seeding, there is no existing snap state information 437 438 si := &snap.SideInfo{ 439 RealName: "foo-gadget", 440 Revision: snap.R(34), 441 SnapID: "foo-id", 442 } 443 snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{ 444 {"meta/gadget.yaml", gadgetYaml}, 445 }) 446 447 s.state.Lock() 448 s.state.Set("seeded", true) 449 450 s.setupModelWithGadget(c, "foo-gadget") 451 452 t := s.state.NewTask("update-gadget-assets", "update gadget") 453 t.Set("snap-setup", &snapstate.SnapSetup{ 454 SideInfo: si, 455 Type: snap.TypeGadget, 456 }) 457 chg := s.state.NewChange("dummy", "...") 458 chg.AddTask(t) 459 460 s.state.Unlock() 461 462 s.settle(c) 463 464 s.state.Lock() 465 defer s.state.Unlock() 466 c.Assert(chg.IsReady(), Equals, true) 467 c.Check(chg.Err(), IsNil) 468 c.Check(t.Status(), Equals, state.DoneStatus) 469 rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget") 470 c.Check(osutil.IsDirectory(rollbackDir), Equals, false) 471 c.Check(s.restartRequests, HasLen, 0) 472 } 473 474 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreBadGadgetYaml(c *C) { 475 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 476 return errors.New("unexpected call") 477 }) 478 defer restore() 479 siCurrent := &snap.SideInfo{ 480 RealName: "foo-gadget", 481 Revision: snap.R(33), 482 SnapID: "foo-id", 483 } 484 si := &snap.SideInfo{ 485 RealName: "foo-gadget", 486 Revision: snap.R(34), 487 SnapID: "foo-id", 488 } 489 snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, [][]string{ 490 {"meta/gadget.yaml", gadgetYaml}, 491 }) 492 // invalid gadget.yaml data 493 snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{ 494 {"meta/gadget.yaml", "foobar"}, 495 }) 496 497 s.state.Lock() 498 s.state.Set("seeded", true) 499 500 s.setupModelWithGadget(c, "foo-gadget") 501 502 snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{ 503 SnapType: "gadget", 504 Sequence: []*snap.SideInfo{siCurrent}, 505 Current: siCurrent.Revision, 506 Active: true, 507 }) 508 509 t := s.state.NewTask("update-gadget-assets", "update gadget") 510 t.Set("snap-setup", &snapstate.SnapSetup{ 511 SideInfo: si, 512 Type: snap.TypeGadget, 513 }) 514 chg := s.state.NewChange("dummy", "...") 515 chg.AddTask(t) 516 517 s.state.Unlock() 518 519 s.settle(c) 520 521 s.state.Lock() 522 defer s.state.Unlock() 523 c.Assert(chg.IsReady(), Equals, true) 524 c.Check(chg.Err(), ErrorMatches, `(?s).*update gadget \(cannot read candidate snap gadget metadata: .*\).*`) 525 c.Check(t.Status(), Equals, state.ErrorStatus) 526 rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget") 527 c.Check(osutil.IsDirectory(rollbackDir), Equals, false) 528 c.Check(s.restartRequests, HasLen, 0) 529 } 530 531 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreParanoidChecks(c *C) { 532 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 533 return errors.New("unexpected call") 534 }) 535 defer restore() 536 siCurrent := &snap.SideInfo{ 537 RealName: "foo-gadget", 538 Revision: snap.R(33), 539 SnapID: "foo-id", 540 } 541 si := &snap.SideInfo{ 542 RealName: "foo-gadget-unexpected", 543 Revision: snap.R(34), 544 SnapID: "foo-id", 545 } 546 547 s.state.Lock() 548 549 s.state.Set("seeded", true) 550 551 s.setupModelWithGadget(c, "foo-gadget") 552 553 snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{ 554 SnapType: "gadget", 555 Sequence: []*snap.SideInfo{siCurrent}, 556 Current: siCurrent.Revision, 557 Active: true, 558 }) 559 560 t := s.state.NewTask("update-gadget-assets", "update gadget") 561 t.Set("snap-setup", &snapstate.SnapSetup{ 562 SideInfo: si, 563 Type: snap.TypeGadget, 564 }) 565 chg := s.state.NewChange("dummy", "...") 566 chg.AddTask(t) 567 568 s.state.Unlock() 569 570 s.settle(c) 571 572 s.state.Lock() 573 defer s.state.Unlock() 574 c.Assert(chg.IsReady(), Equals, true) 575 c.Assert(chg.Err(), ErrorMatches, `(?s).*\(cannot apply gadget assets update from non-model gadget snap "foo-gadget-unexpected", expected "foo-gadget" snap\)`) 576 c.Check(t.Status(), Equals, state.ErrorStatus) 577 c.Check(s.restartRequests, HasLen, 0) 578 } 579 580 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnClassicErrorsOut(c *C) { 581 restore := release.MockOnClassic(true) 582 defer restore() 583 584 restore = devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 585 return errors.New("unexpected call") 586 }) 587 defer restore() 588 589 s.state.Lock() 590 591 s.state.Set("seeded", true) 592 593 t := s.state.NewTask("update-gadget-assets", "update gadget") 594 chg := s.state.NewChange("dummy", "...") 595 chg.AddTask(t) 596 597 s.state.Unlock() 598 599 s.settle(c) 600 601 s.state.Lock() 602 defer s.state.Unlock() 603 c.Assert(chg.IsReady(), Equals, true) 604 c.Check(chg.Err(), ErrorMatches, `(?s).*update gadget \(cannot run update gadget assets task on a classic system\).*`) 605 c.Check(t.Status(), Equals, state.ErrorStatus) 606 } 607 608 type mockUpdater struct{} 609 610 func (m *mockUpdater) Backup() error { return nil } 611 612 func (m *mockUpdater) Rollback() error { return nil } 613 614 func (m *mockUpdater) Update() error { return nil } 615 616 func (s *deviceMgrGadgetSuite) TestUpdateGadgetCallsToGadget(c *C) { 617 siCurrent := &snap.SideInfo{ 618 RealName: "foo-gadget", 619 Revision: snap.R(33), 620 SnapID: "foo-id", 621 } 622 si := &snap.SideInfo{ 623 RealName: "foo-gadget", 624 Revision: snap.R(34), 625 SnapID: "foo-id", 626 } 627 var gadgetCurrentYaml = ` 628 volumes: 629 pc: 630 bootloader: grub 631 structure: 632 - name: foo 633 size: 10M 634 type: bare 635 content: 636 - image: content.img 637 ` 638 var gadgetUpdateYaml = ` 639 volumes: 640 pc: 641 bootloader: grub 642 structure: 643 - name: foo 644 size: 10M 645 type: bare 646 content: 647 - image: content.img 648 update: 649 edition: 2 650 ` 651 snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, [][]string{ 652 {"meta/gadget.yaml", gadgetCurrentYaml}, 653 {"content.img", "some content"}, 654 }) 655 updateInfo := snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{ 656 {"meta/gadget.yaml", gadgetUpdateYaml}, 657 {"content.img", "updated content"}, 658 }) 659 660 expectedRollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34") 661 updaterForStructureCalls := 0 662 restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, rootDir, rollbackDir string, _ gadget.ContentUpdateObserver) (gadget.Updater, error) { 663 updaterForStructureCalls++ 664 665 c.Assert(ps.Name, Equals, "foo") 666 c.Assert(rootDir, Equals, updateInfo.MountDir()) 667 c.Assert(filepath.Join(rootDir, "content.img"), testutil.FileEquals, "updated content") 668 c.Assert(strings.HasPrefix(rollbackDir, expectedRollbackDir), Equals, true) 669 c.Assert(osutil.IsDirectory(rollbackDir), Equals, true) 670 return &mockUpdater{}, nil 671 }) 672 defer restore() 673 674 s.state.Lock() 675 s.state.Set("seeded", true) 676 677 s.setupModelWithGadget(c, "foo-gadget") 678 679 snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{ 680 SnapType: "gadget", 681 Sequence: []*snap.SideInfo{siCurrent}, 682 Current: siCurrent.Revision, 683 Active: true, 684 }) 685 686 t := s.state.NewTask("update-gadget-assets", "update gadget") 687 t.Set("snap-setup", &snapstate.SnapSetup{ 688 SideInfo: si, 689 Type: snap.TypeGadget, 690 }) 691 chg := s.state.NewChange("dummy", "...") 692 chg.AddTask(t) 693 694 s.state.Unlock() 695 696 s.settle(c) 697 698 s.state.Lock() 699 defer s.state.Unlock() 700 c.Assert(chg.IsReady(), Equals, true) 701 c.Check(t.Status(), Equals, state.DoneStatus) 702 c.Check(s.restartRequests, HasLen, 1) 703 c.Check(updaterForStructureCalls, Equals, 1) 704 } 705 706 func (s *deviceMgrGadgetSuite) TestCurrentAndUpdateInfo(c *C) { 707 siCurrent := &snap.SideInfo{ 708 RealName: "foo-gadget", 709 Revision: snap.R(33), 710 SnapID: "foo-id", 711 } 712 si := &snap.SideInfo{ 713 RealName: "foo-gadget", 714 Revision: snap.R(34), 715 SnapID: "foo-id", 716 } 717 718 s.state.Lock() 719 defer s.state.Unlock() 720 721 snapsup := &snapstate.SnapSetup{ 722 SideInfo: si, 723 Type: snap.TypeGadget, 724 } 725 726 model := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 727 "architecture": "amd64", 728 "kernel": "pc-kernel", 729 "gadget": "foo-gadget", 730 "base": "core18", 731 }) 732 deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: model} 733 734 current, err := devicestate.CurrentGadgetInfo(s.state, deviceCtx) 735 c.Assert(current, IsNil) 736 c.Check(err, IsNil) 737 738 snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{ 739 SnapType: "gadget", 740 Sequence: []*snap.SideInfo{siCurrent}, 741 Current: siCurrent.Revision, 742 Active: true, 743 }) 744 745 // mock current first, but gadget.yaml is still missing 746 ci := snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, nil) 747 748 current, err = devicestate.CurrentGadgetInfo(s.state, deviceCtx) 749 750 c.Assert(current, IsNil) 751 c.Assert(err, ErrorMatches, "cannot read current gadget snap details: .*/33/meta/gadget.yaml: no such file or directory") 752 753 // drop gadget.yaml for current snap 754 ioutil.WriteFile(filepath.Join(ci.MountDir(), "meta/gadget.yaml"), []byte(gadgetYaml), 0644) 755 756 current, err = devicestate.CurrentGadgetInfo(s.state, deviceCtx) 757 c.Assert(err, IsNil) 758 c.Assert(current, DeepEquals, &gadget.GadgetData{ 759 Info: &gadget.Info{ 760 Volumes: map[string]*gadget.Volume{ 761 "pc": { 762 Bootloader: "grub", 763 Schema: "gpt", 764 }, 765 }, 766 }, 767 RootDir: ci.MountDir(), 768 }) 769 770 // pending update 771 update, err := devicestate.PendingGadgetInfo(snapsup, deviceCtx) 772 c.Assert(update, IsNil) 773 c.Assert(err, ErrorMatches, "cannot read candidate gadget snap details: cannot find installed snap .* .*/34/meta/snap.yaml") 774 775 ui := snaptest.MockSnapWithFiles(c, snapYaml, si, nil) 776 777 update, err = devicestate.PendingGadgetInfo(snapsup, deviceCtx) 778 c.Assert(update, IsNil) 779 c.Assert(err, ErrorMatches, "cannot read candidate snap gadget metadata: .*/34/meta/gadget.yaml: no such file or directory") 780 781 var updateGadgetYaml = ` 782 volumes: 783 pc: 784 bootloader: grub 785 id: 123 786 ` 787 788 // drop gadget.yaml for update snap 789 ioutil.WriteFile(filepath.Join(ui.MountDir(), "meta/gadget.yaml"), []byte(updateGadgetYaml), 0644) 790 791 update, err = devicestate.PendingGadgetInfo(snapsup, deviceCtx) 792 c.Assert(err, IsNil) 793 c.Assert(update, DeepEquals, &gadget.GadgetData{ 794 Info: &gadget.Info{ 795 Volumes: map[string]*gadget.Volume{ 796 "pc": { 797 Bootloader: "grub", 798 Schema: "gpt", 799 ID: "123", 800 }, 801 }, 802 }, 803 RootDir: ui.MountDir(), 804 }) 805 } 806 807 func (s *deviceMgrGadgetSuite) TestGadgetUpdateBlocksWhenOtherTasks(c *C) { 808 restore := release.MockOnClassic(true) 809 defer restore() 810 811 s.state.Lock() 812 defer s.state.Unlock() 813 814 tUpdate := s.state.NewTask("update-gadget-assets", "update gadget") 815 t1 := s.state.NewTask("other-task-1", "other 1") 816 t2 := s.state.NewTask("other-task-2", "other 2") 817 818 // no other running tasks, does not block 819 c.Assert(devicestate.GadgetUpdateBlocked(tUpdate, nil), Equals, false) 820 821 // list of running tasks actually contains ones that are in the 'running' state 822 t1.SetStatus(state.DoingStatus) 823 t2.SetStatus(state.UndoingStatus) 824 // block on any other running tasks 825 c.Assert(devicestate.GadgetUpdateBlocked(tUpdate, []*state.Task{t1, t2}), Equals, true) 826 } 827 828 func (s *deviceMgrGadgetSuite) TestGadgetUpdateBlocksOtherTasks(c *C) { 829 restore := release.MockOnClassic(true) 830 defer restore() 831 832 s.state.Lock() 833 defer s.state.Unlock() 834 835 tUpdate := s.state.NewTask("update-gadget-assets", "update gadget") 836 tUpdate.SetStatus(state.DoingStatus) 837 t1 := s.state.NewTask("other-task-1", "other 1") 838 t2 := s.state.NewTask("other-task-2", "other 2") 839 840 // block on any other running tasks 841 c.Assert(devicestate.GadgetUpdateBlocked(t1, []*state.Task{tUpdate}), Equals, true) 842 c.Assert(devicestate.GadgetUpdateBlocked(t2, []*state.Task{tUpdate}), Equals, true) 843 844 t2.SetStatus(state.UndoingStatus) 845 // update-gadget should be the only running task, for the sake of 846 // completeness pretend it's one of many running tasks 847 c.Assert(devicestate.GadgetUpdateBlocked(t1, []*state.Task{tUpdate, t2}), Equals, true) 848 849 // not blocking without gadget update task 850 c.Assert(devicestate.GadgetUpdateBlocked(t1, []*state.Task{t2}), Equals, false) 851 } 852 853 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreHybridFirstboot(c *C) { 854 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 855 return errors.New("unexpected call") 856 }) 857 defer restore() 858 859 // simulate first-boot/seeding, there is no existing snap state information 860 861 si := &snap.SideInfo{ 862 RealName: "foo-gadget", 863 Revision: snap.R(34), 864 SnapID: "foo-id", 865 } 866 snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{ 867 {"meta/gadget.yaml", hybridGadgetYaml}, 868 }) 869 870 s.state.Lock() 871 s.state.Set("seeded", true) 872 873 s.setupModelWithGadget(c, "foo-gadget") 874 875 t := s.state.NewTask("update-gadget-assets", "update gadget") 876 t.Set("snap-setup", &snapstate.SnapSetup{ 877 SideInfo: si, 878 Type: snap.TypeGadget, 879 }) 880 chg := s.state.NewChange("dummy", "...") 881 chg.AddTask(t) 882 883 s.state.Unlock() 884 885 s.settle(c) 886 887 s.state.Lock() 888 defer s.state.Unlock() 889 c.Assert(chg.IsReady(), Equals, true) 890 c.Check(chg.Err(), IsNil) 891 c.Check(t.Status(), Equals, state.DoneStatus) 892 rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget") 893 c.Check(osutil.IsDirectory(rollbackDir), Equals, false) 894 c.Check(s.restartRequests, HasLen, 0) 895 } 896 897 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreHybridShouldWork(c *C) { 898 encryption := false 899 s.testUpdateGadgetOnCoreSimple(c, "", encryption, hybridGadgetYaml, "") 900 } 901 902 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreOldIsInvalidNowButShouldWork(c *C) { 903 encryption := false 904 // this is not gadget yaml that we should support, by the UC16/18 905 // rules it actually has two system-boot role partitions, 906 hybridGadgetYamlBroken := hybridGadgetYaml + ` 907 role: system-boot 908 ` 909 s.testUpdateGadgetOnCoreSimple(c, "", encryption, hybridGadgetYamlBroken, hybridGadgetYaml) 910 } 911 912 func (s *deviceMgrGadgetSuite) makeMinimalKernelAssetsUpdateChange(c *C) (chg *state.Change, tsk *state.Task) { 913 s.state.Lock() 914 defer s.state.Unlock() 915 916 siGadget := &snap.SideInfo{ 917 RealName: "foo-gadget", 918 Revision: snap.R(1), 919 SnapID: "foo-gadget-id", 920 } 921 gadgetSnapYaml := "name: foo-gadget\nversion: 1.0\ntype: gadget" 922 gadgetYamlContent := ` 923 volumes: 924 pi: 925 bootloader: grub` 926 snaptest.MockSnapWithFiles(c, gadgetSnapYaml, siGadget, [][]string{ 927 {"meta/gadget.yaml", gadgetYamlContent}, 928 }) 929 s.setupModelWithGadget(c, "foo-gadget") 930 snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{ 931 SnapType: "gadget", 932 Sequence: []*snap.SideInfo{siGadget}, 933 Current: siGadget.Revision, 934 Active: true, 935 }) 936 937 snapKernelYaml := "name: pc-kernel\nversion: 1.0\ntype: kernel" 938 siCurrent := &snap.SideInfo{ 939 RealName: "pc-kernel", 940 Revision: snap.R(33), 941 SnapID: "foo-id", 942 } 943 snaptest.MockSnapWithFiles(c, snapKernelYaml, siCurrent, nil) 944 siNext := &snap.SideInfo{ 945 RealName: "pc-kernel", 946 Revision: snap.R(34), 947 SnapID: "foo-id", 948 } 949 snaptest.MockSnapWithFiles(c, snapKernelYaml, siNext, nil) 950 snapstate.Set(s.state, "pc-kernel", &snapstate.SnapState{ 951 SnapType: "kernel", 952 Sequence: []*snap.SideInfo{siNext, siCurrent}, 953 Current: siCurrent.Revision, 954 Active: true, 955 }) 956 957 s.bootloader.SetBootVars(map[string]string{ 958 "snap_core": "core_1.snap", 959 "snap_kernel": "pc-kernel_33.snap", 960 }) 961 962 tsk = s.state.NewTask("update-gadget-assets", "update gadget") 963 tsk.Set("snap-setup", &snapstate.SnapSetup{ 964 SideInfo: siNext, 965 Type: snap.TypeKernel, 966 }) 967 chg = s.state.NewChange("dummy", "...") 968 chg.AddTask(tsk) 969 970 return chg, tsk 971 } 972 973 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreFromKernel(c *C) { 974 var updateCalled int 975 var passedRollbackDir string 976 977 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error { 978 updateCalled++ 979 passedRollbackDir = path 980 981 c.Check(strings.HasSuffix(current.RootDir, "/snap/foo-gadget/1"), Equals, true) 982 c.Check(strings.HasSuffix(update.RootDir, "/snap/foo-gadget/1"), Equals, true) 983 c.Check(strings.HasSuffix(current.KernelRootDir, "/snap/pc-kernel/33"), Equals, true) 984 c.Check(strings.HasSuffix(update.KernelRootDir, "/snap/pc-kernel/34"), Equals, true) 985 986 // KernelUpdatePolicy is used 987 c.Check(reflect.ValueOf(policy), DeepEquals, reflect.ValueOf(gadget.UpdatePolicyFunc(gadget.KernelUpdatePolicy))) 988 return nil 989 }) 990 defer restore() 991 992 chg, t := s.makeMinimalKernelAssetsUpdateChange(c) 993 devicestate.SetBootOkRan(s.mgr, true) 994 995 s.state.Lock() 996 s.state.Set("seeded", true) 997 s.state.Unlock() 998 999 s.settle(c) 1000 1001 s.state.Lock() 1002 defer s.state.Unlock() 1003 c.Assert(chg.IsReady(), Equals, true) 1004 c.Check(chg.Err(), IsNil) 1005 c.Check(t.Status(), Equals, state.DoneStatus) 1006 c.Check(updateCalled, Equals, 1) 1007 rollbackDir := filepath.Join(dirs.SnapRollbackDir, "pc-kernel_34") 1008 c.Check(rollbackDir, Equals, passedRollbackDir) 1009 } 1010 1011 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreFromKernelRemodel(c *C) { 1012 var updateCalled int 1013 var passedRollbackDir string 1014 1015 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error { 1016 updateCalled++ 1017 passedRollbackDir = path 1018 1019 c.Check(strings.HasSuffix(current.RootDir, "/snap/foo-gadget/1"), Equals, true) 1020 c.Check(strings.HasSuffix(update.RootDir, "/snap/foo-gadget/1"), Equals, true) 1021 c.Check(strings.HasSuffix(current.KernelRootDir, "/snap/pc-kernel/33"), Equals, true) 1022 c.Check(strings.HasSuffix(update.KernelRootDir, "/snap/pc-kernel/34"), Equals, true) 1023 1024 // KernelUpdatePolicy is used even when we remodel 1025 c.Check(reflect.ValueOf(policy), DeepEquals, reflect.ValueOf(gadget.UpdatePolicyFunc(gadget.KernelUpdatePolicy))) 1026 return nil 1027 }) 1028 defer restore() 1029 1030 chg, t := s.makeMinimalKernelAssetsUpdateChange(c) 1031 devicestate.SetBootOkRan(s.mgr, true) 1032 1033 newModel := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 1034 "architecture": "amd64", 1035 "kernel": "pc-kernel", 1036 "gadget": "foo-gadget", 1037 "base": "core18", 1038 "revision": "1", 1039 }) 1040 1041 s.state.Lock() 1042 // pretend we are remodeling 1043 chg.Set("new-model", string(asserts.Encode(newModel))) 1044 s.state.Set("seeded", true) 1045 s.state.Unlock() 1046 1047 s.settle(c) 1048 1049 s.state.Lock() 1050 defer s.state.Unlock() 1051 c.Assert(chg.IsReady(), Equals, true) 1052 c.Check(chg.Err(), IsNil) 1053 c.Check(t.Status(), Equals, state.DoneStatus) 1054 c.Check(updateCalled, Equals, 1) 1055 rollbackDir := filepath.Join(dirs.SnapRollbackDir, "pc-kernel_34") 1056 c.Check(rollbackDir, Equals, passedRollbackDir) 1057 }